@commercelayer/agent-cli 1.0.0-dev.2 → 1.0.0-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/builtins/loops.js +36 -14
- package/dist/builtins/sandbox.js +4 -6
- package/dist/config.js +24 -7
- package/dist/engine.js +7 -6
- package/dist/secrets.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ This is the 1.0.0 line, in development on the `clean-slate` branch. It replaces
|
|
|
34
34
|
| `c11r auth login [--client-id] [--secret-file] [--keep-file]` | The human's own Commerce Layer login, from their Provisioning API application. See below. |
|
|
35
35
|
| `c11r auth logout`, `auth switch <slug> [--global]` | Forgetting the login; the organization commands act on. |
|
|
36
36
|
|
|
37
|
-
A loop policy allows verbs: a bare verb is the skill's own noun (`approve`), `<noun>.<verb>` another Manage noun's verb on the same item (`payments.capture`). Verbs that delete or return money are never open to a policy.
|
|
37
|
+
A loop runs a skill a Manage noun's schema names in `loop.skill` (`manage-orders`, `manage-payments`, `playbook-mail-to-order` for carts); create and update refuse any other skill, and any verb that loop does not list, with the allowed values. A loop policy allows verbs: a bare verb is the skill's own noun (`approve`), `<noun>.<verb>` another Manage noun's verb on the same item (`payments.capture`). Verbs that delete or return money are never open to a policy.
|
|
38
38
|
|
|
39
39
|
## Login, organization tokens and secrets
|
|
40
40
|
|
|
@@ -50,7 +50,7 @@ Never from a flag or the environment. c11r exchanges the pair for a two hour tok
|
|
|
50
50
|
|
|
51
51
|
`organizations create` and `organizations connect <slug>` answer an organization token once: it is stored like a sandbox token. `connect --revoke-others` also ends every other token of that organization, and asks for confirmation. With a login and no organization token for the organization named, an organization noun stops with `no_credentials` and the next step `c11r organizations connect <slug>`. `members` and `roles` with an organization token and no login are sent with that token, and the API says what is needed: a claim for a sandbox, a login for a claimed organization. `organizations` with no login stops here with `login_required`.
|
|
52
52
|
|
|
53
|
-
`credentials create` and `credentials token` answer a secret once. c11r writes it to a file with mode 600 and prints the file, the format and a hint, never the value: by default `~/.config/c11r/secrets/<org>/<name>.env` (`<id>-token.env` for a token), or `--out <path>`, as `KEY=value` lines or with `--format json` one JSON object. The file is checked before the request is sent; one there already is replaced only with `--yes`. A file inside a git repository that git does not ignore gets the warning `secret_not_ignored`. With `--dry-run` nothing is written and `secret_file` says where it would go. Each file records the record it belongs to. A `delete` of a record whose noun answers secrets (`credentials`, `webhooks`) removes the files c11r wrote for it in `~/.config/c11r/secrets/<org>/` (a credential's secret and tokens, a webhook's shared secret), found by the id, name or code given, and lists them in `removed_files`; `sandbox destroy` removes the whole folder of that organization and names it in `forgotten.secrets`. A file written with `--out` is not tracked and stays: the warning `out_files_not_tracked` says so
|
|
53
|
+
`credentials create` and `credentials token` answer a secret once. c11r writes it to a file with mode 600 and prints the file, the format and a hint, never the value: by default `~/.config/c11r/secrets/<org>/<name>.env` (`<id>-token.env` for a token), or `--out <path>`, as `KEY=value` lines or with `--format json` one JSON object. The file is checked before the request is sent; one there already is replaced only with `--yes`. A file inside a git repository that git does not ignore gets the warning `secret_not_ignored`. With `--dry-run` nothing is written and `secret_file` says where it would go. Each file records the record it belongs to. A `delete` of a record whose noun answers secrets (`credentials`, `webhooks`) removes the files c11r wrote for it in `~/.config/c11r/secrets/<org>/` (a credential's secret and tokens, a webhook's shared secret), found by the id, name or code given, and lists them in `removed_files`; `sandbox destroy` removes the whole folder of that organization and names it in `forgotten.secrets`. A file written with `--out` is not tracked and stays: the warning `out_files_not_tracked` says so on the delete of the record whose secret went to `--out` on this machine, and on `sandbox destroy` of an organization that had one. Any other delete has no such warning. A token file carries `CL_BASE_ENDPOINT` beside the token. `sales-calls book` answers a cancel token once: it is kept in `credentials.json` and `sales-calls cancel <booking>` sends it.
|
|
54
54
|
|
|
55
55
|
## Global flags
|
|
56
56
|
|
package/dist/builtins/loops.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// schedules the agent repeats it every `every`.
|
|
5
5
|
//
|
|
6
6
|
// A policy allows verbs: a bare verb is the skill's own noun (approve), <noun>.<verb> another
|
|
7
|
-
// Manage noun's verb on the same item (payments.capture).
|
|
8
|
-
// loop.
|
|
7
|
+
// Manage noun's verb on the same item (payments.capture). The skill must be one a Manage noun's
|
|
8
|
+
// schema names in loop.skill (a noun's own skill is not always its loop's: carts loops run
|
|
9
|
+
// playbook-mail-to-order), and each verb in that loop.verbs; the API checks again on every pass.
|
|
9
10
|
import fs from 'node:fs';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import YAML from 'yaml';
|
|
@@ -13,11 +14,11 @@ import { projectDir } from '../config.js';
|
|
|
13
14
|
import { CliError, closest, fail, fixWith } from '../errors.js';
|
|
14
15
|
import { envelope } from '../http.js';
|
|
15
16
|
import { parseSchema } from '../schema.js';
|
|
16
|
-
import { apiSchema } from '../schemas.js';
|
|
17
|
+
import { apiNouns, apiSchema } from '../schemas.js';
|
|
17
18
|
const NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
18
19
|
const EVERY = /^[1-9]\d*(m|h|d)$/;
|
|
19
20
|
const policyFlags = (required) => [
|
|
20
|
-
{ name: 'skill', type: 'string', required, description: 'The
|
|
21
|
+
{ name: 'skill', type: 'string', required, description: 'The skill or playbook the loop runs, as in manage-orders or playbook-mail-to-order: one a noun\'s schema names in loop.skill.' },
|
|
21
22
|
{ name: 'every', type: 'string', required, description: 'How often a pass should run, as in 15m, 2h or 1d. The scheduler reads it; the CLI does not wait.' },
|
|
22
23
|
{ name: 'allow', type: 'array', required, description: 'A verb the policy allows, repeated: approve, payments.capture. Only verbs the noun\'s schema lists in loop.verbs.' },
|
|
23
24
|
{ name: 'max_total', type: 'array', description: 'The largest order total acted on, in major units: 500 for every currency, or EUR=500, repeated per currency. Above it, the item waits for the human.' },
|
|
@@ -166,24 +167,45 @@ function maxTotal(items) {
|
|
|
166
167
|
return [ccy, amount(n)];
|
|
167
168
|
}));
|
|
168
169
|
}
|
|
169
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* The skill and the policy's verbs against the same source loops run uses: each noun schema's loop,
|
|
172
|
+
* { skill, verbs }. A skill no schema's loop names is refused with the ones that are, and so is a
|
|
173
|
+
* verb its loop does not list.
|
|
174
|
+
*/
|
|
170
175
|
async function checkPolicy(def) {
|
|
171
176
|
if (!def.policy.allow.length)
|
|
172
177
|
fail('missing_argument', 'A loop needs at least one --allow.', { path: 'allow' });
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
const skills = await loopSkills();
|
|
179
|
+
const allowed = skills.get(def.skill);
|
|
180
|
+
if (!allowed) {
|
|
181
|
+
const names = [...skills.keys()].sort();
|
|
182
|
+
fail('invalid_value', `--skill ${def.skill} runs no loop. A loop runs ${names.length ? names.join(', ') : 'no skill yet'}.`, {
|
|
183
|
+
path: 'skill',
|
|
184
|
+
did_you_mean: closest(def.skill, names),
|
|
185
|
+
details: { values: names },
|
|
186
|
+
next_step: fixWith('c11r loops create --help'),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
180
189
|
for (const verb of def.policy.allow) {
|
|
181
|
-
if (!allowed.includes(verb)) {
|
|
182
|
-
fail('policy_forbidden', `A ${def.skill} loop may not allow ${verb}. It may allow ${allowed.join(', ')}.`, { path: 'allow', did_you_mean: closest(verb, allowed), details: { loopable: allowed }, next_step: fixWith(`c11r schema ${noun}`) });
|
|
190
|
+
if (!allowed.verbs.includes(verb)) {
|
|
191
|
+
fail('policy_forbidden', `A ${def.skill} loop may not allow ${verb}. It may allow ${allowed.verbs.join(', ')}.`, { path: 'allow', did_you_mean: closest(verb, allowed.verbs), details: { loopable: allowed.verbs }, next_step: fixWith(`c11r schema ${allowed.noun}`) });
|
|
183
192
|
}
|
|
184
193
|
}
|
|
185
194
|
return [];
|
|
186
195
|
}
|
|
196
|
+
/** The skills a loop can run, from the schemas of the nouns that have a loop: skill to its noun and verbs. */
|
|
197
|
+
async function loopSkills() {
|
|
198
|
+
const nouns = (await apiNouns()).filter((n) => n.group === 'manage' && n.served !== false);
|
|
199
|
+
const schemas = await Promise.all(nouns.map((n) => apiSchema(n.noun, []).catch(() => null)));
|
|
200
|
+
const out = new Map();
|
|
201
|
+
for (const s of schemas) {
|
|
202
|
+
if (!s?.loop?.skill)
|
|
203
|
+
continue;
|
|
204
|
+
const seen = out.get(s.loop.skill);
|
|
205
|
+
out.set(s.loop.skill, { noun: seen?.noun ?? s.noun, verbs: [...new Set([...(seen?.verbs ?? []), ...s.loop.verbs])] });
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
187
209
|
async function remove(run, name, file) {
|
|
188
210
|
one(file, name);
|
|
189
211
|
if (run.globals.dryRun)
|
package/dist/builtins/sandbox.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// reset moves both to the new organization; destroy forgets them. The token is printed never,
|
|
5
5
|
// not even by create: only its hint.
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
|
-
import { credentialsFile, defaultOrg,
|
|
7
|
+
import { credentialsFile, defaultOrg, forgetOutWritten, storedLogin, forgetOrg, LINK_FILE, readLink, removeLink, renameOrg, storedOrgs, storeOrg, writeLink, hintOf } from '../config.js';
|
|
8
8
|
import { CliError, fail } from '../errors.js';
|
|
9
9
|
import { apiUrl, envelope, request } from '../http.js';
|
|
10
10
|
import { parseSchema } from '../schema.js';
|
|
@@ -28,7 +28,7 @@ export const sandbox = {
|
|
|
28
28
|
live: false,
|
|
29
29
|
body: [
|
|
30
30
|
{ name: 'name', type: 'string', description: 'The organization name; without it, an invented one. A name starting zztest marks a test organization.' },
|
|
31
|
-
{ name: 'seed', type: 'boolean', description: 'Loads the demo dataset: two markets, six products, shipping and a
|
|
31
|
+
{ name: 'seed', type: 'boolean', description: 'Loads the demo dataset: two markets, six products, shipping and a manual payment setting (Wire transfer), enough to place a test order at once.' },
|
|
32
32
|
],
|
|
33
33
|
effect: 'Creates a sandbox, stores its token on this machine (never printed) and links this folder to it with .c11r.json. With --seed it answers once the seed is applied, in about 20 seconds.',
|
|
34
34
|
},
|
|
@@ -124,8 +124,7 @@ async function destroy(run) {
|
|
|
124
124
|
// here. The sandbox is gone either way, so this machine forgets it too.
|
|
125
125
|
if (!(e instanceof CliError && e.code === 'unauthorized') || run.globals.dryRun || !run.org.org)
|
|
126
126
|
throw e;
|
|
127
|
-
const out =
|
|
128
|
-
markOutWritten(run.org.org, false);
|
|
127
|
+
const out = forgetOutWritten(run.org.org);
|
|
129
128
|
forgetOrg(run.org.org);
|
|
130
129
|
const link = removeLink(run.org.org);
|
|
131
130
|
const secrets = removeOrgSecrets(run.org.org);
|
|
@@ -136,8 +135,7 @@ async function destroy(run) {
|
|
|
136
135
|
const org = r.data?.org ?? run.org.org;
|
|
137
136
|
if (!org)
|
|
138
137
|
return r;
|
|
139
|
-
const out =
|
|
140
|
-
markOutWritten(org, false);
|
|
138
|
+
const out = forgetOutWritten(org);
|
|
141
139
|
forgetOrg(org);
|
|
142
140
|
const link = removeLink(org);
|
|
143
141
|
const secrets = removeOrgSecrets(org);
|
package/dist/config.js
CHANGED
|
@@ -110,20 +110,37 @@ export function setDefaultOrg(org) {
|
|
|
110
110
|
delete c.default_org;
|
|
111
111
|
writeCredentials(c);
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
/** A record's secret went to an --out file: remembered so its delete can say that file stays. */
|
|
114
|
+
export function markOutWritten(org, record) {
|
|
115
|
+
const keys = record.keys.filter(Boolean);
|
|
116
|
+
if (!keys.length)
|
|
117
|
+
return;
|
|
118
|
+
const c = readCredentials();
|
|
119
|
+
const all = { ...(c.out_written ?? {}) };
|
|
120
|
+
const list = (all[org] ?? []).filter((r) => !(r.noun === record.noun && r.keys.some((k) => keys.includes(k))));
|
|
121
|
+
all[org] = [...list, { noun: record.noun, keys }];
|
|
122
|
+
c.out_written = all;
|
|
123
|
+
writeCredentials(c);
|
|
115
124
|
}
|
|
116
|
-
|
|
125
|
+
/**
|
|
126
|
+
* After a delete: whether that record had a secret written with --out, which is then forgotten.
|
|
127
|
+
* With no record named, the organization goes (sandbox destroy).
|
|
128
|
+
*/
|
|
129
|
+
export function forgetOutWritten(org, record) {
|
|
117
130
|
const c = readCredentials();
|
|
131
|
+
const list = c.out_written?.[org] ?? [];
|
|
132
|
+
const hit = (r) => !record || (r.noun === record.noun && r.keys.some((k) => record.keys.includes(k)));
|
|
133
|
+
if (!list.some(hit))
|
|
134
|
+
return false;
|
|
135
|
+
const rest = list.filter((r) => !hit(r));
|
|
118
136
|
const all = { ...(c.out_written ?? {}) };
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
if (written)
|
|
122
|
-
all[org] = true;
|
|
137
|
+
if (rest.length)
|
|
138
|
+
all[org] = rest;
|
|
123
139
|
else
|
|
124
140
|
delete all[org];
|
|
125
141
|
c.out_written = all;
|
|
126
142
|
writeCredentials(c);
|
|
143
|
+
return true;
|
|
127
144
|
}
|
|
128
145
|
export function storedBooking(id) {
|
|
129
146
|
const b = readCredentials().bookings?.[id];
|
package/dist/engine.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// destructive verb, then the request. A plain noun needs no code here: its schema says
|
|
5
5
|
// everything. Every answer is the API's envelope, { data, state, warnings, next }.
|
|
6
6
|
import { parseCommand, parseFields, toFlag } from './argv.js';
|
|
7
|
-
import {
|
|
7
|
+
import { forgetOutWritten, raiseProblem, resolveOrg } from './config.js';
|
|
8
8
|
import { confirm, terminal } from './confirm.js';
|
|
9
9
|
import { CliError, closest, fail, fixWith } from './errors.js';
|
|
10
10
|
import { indexHelp, nounHelp, verbHelp } from './help.js';
|
|
@@ -118,9 +118,9 @@ export async function execute(argv, opts = {}) {
|
|
|
118
118
|
return filesGone(secretOf(r, run, secretPlan), run);
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
121
|
-
* A delete
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* A delete on a noun that answers secrets: the files c11r wrote for that record in the secrets
|
|
122
|
+
* folder go with it, listed in removed_files. out_files_not_tracked only when that record's secret
|
|
123
|
+
* was written with --out on this machine: that file stays where it was written.
|
|
124
124
|
*/
|
|
125
125
|
function filesGone(r, run) {
|
|
126
126
|
if (run.verb.name !== 'delete' || run.globals.dryRun || !run.org.org)
|
|
@@ -128,8 +128,9 @@ function filesGone(r, run) {
|
|
|
128
128
|
if (!Object.values(run.schema.verbs).some((v) => v.secret))
|
|
129
129
|
return r;
|
|
130
130
|
const data = (r.data && typeof r.data === 'object' ? r.data : {});
|
|
131
|
-
const
|
|
132
|
-
const
|
|
131
|
+
const keys = [run.parsed.id, data.id, data.name, data.code].filter((k) => typeof k === 'string' && k !== '');
|
|
132
|
+
const removed = removeRecordFiles(run.org.org, run.schema.noun, keys);
|
|
133
|
+
const warn = forgetOutWritten(run.org.org, { noun: run.schema.noun, keys });
|
|
133
134
|
return { ...r, data: { ...data, removed_files: removed }, warnings: [...r.warnings, ...(warn ? [OUT_NOT_TRACKED] : [])] };
|
|
134
135
|
}
|
|
135
136
|
/** A secret in the answer goes to its file; a dry run says where it would go. */
|
package/dist/secrets.js
CHANGED
|
@@ -97,9 +97,9 @@ export function writeSecret(r, plan, ctx) {
|
|
|
97
97
|
const created = ctx.verb === 'create';
|
|
98
98
|
fail('secret_not_written', `The API answered the ${secret.kind ?? 'secret'} of credential ${id} once, and c11r could not write it to ${file}: ${e.message}. It was not printed, and the API cannot show it again. ${created ? `c11r credentials token ${id} gives a working token now; for a new secret, create another credential and delete ${id}.` : `c11r credentials token ${id} gives another token.`}`, { path: 'out', next_step: { action: 'fix_request', command: `c11r credentials token ${id}` }, details: { credential_id: credentialId ?? null, file } });
|
|
99
99
|
}
|
|
100
|
-
// An --out file is not removed with its
|
|
100
|
+
// An --out file is not removed with its record: remember which record has one, for the warning.
|
|
101
101
|
if (plan.fixed && ctx.org)
|
|
102
|
-
markOutWritten(ctx.org,
|
|
102
|
+
markOutWritten(ctx.org, { noun: ctx.noun, keys: [record.id, record.name, record.code, ctx.id].filter((k) => typeof k === 'string' && k !== '') });
|
|
103
103
|
// The API says the secret is shown once: c11r kept it in the file, and printed none of it.
|
|
104
104
|
const warnings = r.warnings.filter((w) => w.code !== 'secret_shown_once' && w.code !== 'token_shown_once');
|
|
105
105
|
if (!ignoredByGit(file))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercelayer/agent-cli",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.4",
|
|
4
4
|
"description": "c11r: the Commerce Layer CLI for coding agents. One grammar, c11r <noun> <verb>, from a sandbox to a running organization.",
|
|
5
5
|
"homepage": "https://commercelayer.ai",
|
|
6
6
|
"keywords": [
|