1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
|
#include "commands.h"
#include <stdlib.h>
#include <string.h>
#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];
|