#include "commands.h" #include #include #include "cmd_util.h" #include "version.h" /* ------------------------------------------------------------------ */ /* discovery */ /* ------------------------------------------------------------------ */ static yyjson_mut_val *command_json(yyjson_mut_doc *doc, const struct command *c) { yyjson_mut_val *o = yyjson_mut_obj(doc); yyjson_mut_obj_add_strcpy(doc, o, "name", c->name); yyjson_mut_obj_add_strcpy(doc, o, "summary", c->summary); yyjson_mut_obj_add_bool(doc, o, "mutating", c->mutating != 0); yyjson_mut_obj_add_bool(doc, o, "dry_run", c->dry_run != 0); yyjson_mut_val *perm = yyjson_mut_obj(doc); yyjson_mut_obj_add_strcpy(doc, perm, "role", perm_name(c->perm)); const char *scope = perm_scope(c->perm); if (scope) yyjson_mut_obj_add_strcpy(doc, perm, "scope", scope); else yyjson_mut_obj_add_null(doc, perm, "scope"); yyjson_mut_obj_add_bool(doc, perm, "require_org", c->need_org != 0); yyjson_mut_obj_add_val(doc, o, "permission", perm); yyjson_mut_val *args = yyjson_mut_arr(doc); for (size_t i = 0; i < c->nargs; i++) { const struct cmd_arg *a = &c->args[i]; yyjson_mut_val *ao = yyjson_mut_arr_add_obj(doc, args); yyjson_mut_obj_add_strcpy(doc, ao, "name", a->name); yyjson_mut_obj_add_strcpy(doc, ao, "type", arg_type_name(a->type)); yyjson_mut_obj_add_bool(doc, ao, "required", a->required != 0); if (a->def) yyjson_mut_obj_add_strcpy(doc, ao, "default", a->def); if (a->values) { yyjson_mut_val *vals = yyjson_mut_arr(doc); const char *p = a->values; while (*p) { const char *comma = strchr(p, ','); size_t len = comma ? (size_t)(comma - p) : strlen(p); yyjson_mut_arr_add_strn(doc, vals, p, len); if (!comma) break; p = comma + 1; } yyjson_mut_obj_add_val(doc, ao, "values", vals); } if (a->desc) yyjson_mut_obj_add_strcpy(doc, ao, "description", a->desc); } yyjson_mut_obj_add_val(doc, o, "args", args); return o; } yyjson_mut_val *commands_describe(yyjson_mut_doc *doc) { yyjson_mut_val *arr = yyjson_mut_arr(doc); for (size_t t = 0; t < g_command_tables_count; t++) { const struct cmd_table *tab = g_command_tables[t]; for (size_t i = 0; i < tab->n; i++) yyjson_mut_arr_add_val(arr, command_json(doc, &tab->cmds[i])); } return arr; } static yyjson_mut_val *h_describe(struct req *r) { const char *name = arg_str(r->args, "cmd"); yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_int(r->rdoc, o, "protocol", BOKF_PROTOCOL_VERSION); yyjson_mut_obj_add_strcpy(r->rdoc, o, "server_version", BOKF_VERSION); if (name) { const struct command *c = command_find(name); if (!c) return fail(r, "NOT_FOUND", "unknown command"); yyjson_mut_obj_add_val(r->rdoc, o, "command", command_json(r->rdoc, c)); return o; } yyjson_mut_obj_add_val(r->rdoc, o, "commands", commands_describe(r->rdoc)); return o; } static const char AGENT_INSTRUCTIONS[] = "# bokf agent instructions (protocol v1)\n" "\n" "You operate a Swedish bookkeeping system through its JSON API.\n" "\n" "## Authentication\n" "- Open one session with `session.open` using an API token:\n" " {\"method\":\"token\",\"token\":\"bokf_...\"}.\n" "- Send the returned session id as `session` on every request.\n" "- Never handle a password unless the human explicitly logs you in that way.\n" "\n" "## Ground rules\n" "- Amounts are integer öre. Dates are YYYY-MM-DD. Account numbers are strings.\n" "- For every mutating command: call it once with `dry_run:true`, inspect the\n" " result, then call again with identical args and a `client_ref` to post.\n" "- Posted data cannot be edited or deleted. Corrections are new vouchers via\n" " `voucher.correct`; the original always remains visible.\n" "- Postings must balance: at least two rows, sum debit == sum credit.\n" "- Locked periods (`PERIOD_LOCKED`) and closed fiscal years\n" " (`FISCAL_YEAR_CLOSED`) are hard stops. Ask the human.\n" "- `fiscal_year.close`, `period.lock`, `sie.import` and `backup.snapshot`\n" " are irreversible or sensitive: confirm with the human first.\n" "- Underlag: upload with `attachment.put`, link with `attachment_ids` when\n" " posting.\n" "- On `CONFLICT` or `replayed:true`, fetch the existing object instead of\n" " retrying. On `RATE_LIMITED`, back off and report.\n" "- Konteringsmallar: list with `template.list`, apply with `voucher.post`\n" " `{\"template\":\"name\",\"x\":1250}` where x is kronor; formulas use x,\n" " numbers and + - * /. Positive results debit, negative credit.\n" "- Bank: `bank.import` stores a SEB CSV statement as read-only evidence and\n" " `bank.list` suggests already-posted vouchers to match with `bank.match`;\n" " reconciliation never posts vouchers by itself.\n" "- Fakturering: `customer.create` keeps the customer register; `invoice.preview`\n" " renders a draft without consuming a number, and `invoice.issue` takes the\n" " next number, stores the PDF and posts the voucher in one transaction.\n" " `invoice.send` e-mails the stored PDF; it needs the org's SMTP settings\n" " and sends to the customer's address unless `to` overrides it.\n" "- Payroll: `payroll.run_preview` then `payroll.run_post` per period;\n" " `payroll.agi` returns the values for the manual arbetsgivardeklaration;\n" " `payroll.payslip` renders and `payroll.payslip_mail` sends the payslip.\n" "\n" "## Discovery\n" "- `describe` lists every implemented command with permissions.\n" "- Reports are read-only: `report.trial_balance`, `report.balance_sheet`,\n" " `report.income_statement`, `report.general_ledger`, `report.vat`.\n"; static yyjson_mut_val *h_agent_instructions(struct req *r) { yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_strcpy(r->rdoc, o, "format", "markdown"); yyjson_mut_obj_add_strcpy(r->rdoc, o, "content", AGENT_INSTRUCTIONS); return o; } static const struct cmd_arg args_describe[] = { { "cmd", ARG_STR, 0, NULL, NULL, "Command name; omit for the full catalogue" }, }; const struct command g_cmd_discovery[] = { { "describe", "List implemented commands and permissions", PERM_READ, 0, 0, 0, h_describe, CMD_ARGS(args_describe) }, { "agent.instructions", "Workflow rules for agents", PERM_READ, 0, 0, 0, h_agent_instructions, NULL, 0 }, }; const struct cmd_table g_cmd_table_discovery = { g_cmd_discovery, sizeof g_cmd_discovery / sizeof g_cmd_discovery[0] }; const struct command *command_find(const char *name) { if (!name) return NULL; for (size_t t = 0; t < g_command_tables_count; t++) { const struct cmd_table *tab = g_command_tables[t]; for (size_t i = 0; i < tab->n; i++) if (strcmp(tab->cmds[i].name, name) == 0) return &tab->cmds[i]; } return NULL; } const char *perm_name(enum cmd_perm p) { switch (p) { case PERM_PUBLIC: return "public"; case PERM_READ: return "viewer"; case PERM_WRITE: return "bookkeeper"; case PERM_OWNER: return "owner"; case PERM_ADMIN: return "admin"; } return "unknown"; } const char *perm_scope(enum cmd_perm p) { switch (p) { case PERM_PUBLIC: return NULL; case PERM_READ: return "read"; case PERM_WRITE: return "write"; case PERM_OWNER: case PERM_ADMIN: return "admin"; } return NULL; } extern const struct cmd_table g_cmd_table_auth; extern const struct cmd_table g_cmd_table_org; extern const struct cmd_table g_cmd_table_users; extern const struct cmd_table g_cmd_table_audit; extern const struct cmd_table g_cmd_table_accounts; extern const struct cmd_table g_cmd_table_fiscal; extern const struct cmd_table g_cmd_table_vouchers; extern const struct cmd_table g_cmd_table_templates; extern const struct cmd_table g_cmd_table_settings; extern const struct cmd_table g_cmd_table_customers; extern const struct cmd_table g_cmd_table_invoices; extern const struct cmd_table g_cmd_table_rules; extern const struct cmd_table g_cmd_table_attachments; extern const struct cmd_table g_cmd_table_reports; extern const struct cmd_table g_cmd_table_sru; extern const struct cmd_table g_cmd_table_sie; extern const struct cmd_table g_cmd_table_bokslut; extern const struct cmd_table g_cmd_table_bank; extern const struct cmd_table g_cmd_table_employees; extern const struct cmd_table g_cmd_table_payroll; const struct cmd_table *const g_command_tables[] = { &g_cmd_table_auth, &g_cmd_table_org, &g_cmd_table_users, &g_cmd_table_discovery, &g_cmd_table_audit, &g_cmd_table_accounts, &g_cmd_table_fiscal, &g_cmd_table_vouchers, &g_cmd_table_bokslut, &g_cmd_table_settings, &g_cmd_table_bank, &g_cmd_table_rules, &g_cmd_table_templates, &g_cmd_table_attachments, &g_cmd_table_reports, &g_cmd_table_sru, &g_cmd_table_sie, &g_cmd_table_customers, &g_cmd_table_invoices, &g_cmd_table_employees, &g_cmd_table_payroll, }; const size_t g_command_tables_count = sizeof g_command_tables / sizeof g_command_tables[0];