@intentic/sandbox-contract 1.240.0 → 1.242.0
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 +52 -16
- package/dist/agent-catalog.d.ts +5 -28
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +21 -157
- package/dist/agent-catalog.js.map +1 -1
- package/dist/agent-run-model.d.ts +3 -2
- package/dist/agent-run-model.d.ts.map +1 -1
- package/dist/agent-run-model.js +9 -6
- package/dist/agent-run-model.js.map +1 -1
- package/dist/agent-runtimes.d.ts +27 -0
- package/dist/agent-runtimes.d.ts.map +1 -0
- package/dist/agent-runtimes.js +113 -0
- package/dist/agent-runtimes.js.map +1 -0
- package/dist/approvals-execution.d.ts +11 -0
- package/dist/approvals-execution.d.ts.map +1 -0
- package/dist/approvals-execution.js +44 -0
- package/dist/approvals-execution.js.map +1 -0
- package/dist/arrival.d.ts +151 -0
- package/dist/arrival.d.ts.map +1 -0
- package/dist/arrival.js +56 -0
- package/dist/arrival.js.map +1 -0
- package/dist/card-status.d.ts +5 -0
- package/dist/card-status.d.ts.map +1 -0
- package/dist/card-status.js +74 -0
- package/dist/card-status.js.map +1 -0
- package/dist/chores/chores.d.ts.map +1 -1
- package/dist/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/command-classes.d.ts +0 -1
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +9 -2
- package/dist/command-classes.js.map +1 -1
- package/dist/command-run.d.ts +10 -0
- package/dist/command-run.d.ts.map +1 -0
- package/dist/command-run.js +46 -0
- package/dist/command-run.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +991 -771
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +345 -337
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +20 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/approvals.contract.d.ts +99 -0
- package/dist/contracts/approvals.contract.d.ts.map +1 -0
- package/dist/contracts/approvals.contract.js +32 -0
- package/dist/contracts/approvals.contract.js.map +1 -0
- package/dist/contracts/ci.contract.d.ts +1 -4
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +1 -9
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/claude.contract.d.ts +9 -0
- package/dist/contracts/claude.contract.d.ts.map +1 -1
- package/dist/contracts/cursor.contract.d.ts +6 -0
- package/dist/contracts/cursor.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +63 -1
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +22 -4
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/grok.contract.d.ts +3 -0
- package/dist/contracts/grok.contract.d.ts.map +1 -1
- package/dist/contracts/keys.contract.d.ts +81 -0
- package/dist/contracts/keys.contract.d.ts.map +1 -0
- package/dist/contracts/keys.contract.js +51 -0
- package/dist/contracts/keys.contract.js.map +1 -0
- package/dist/contracts/panels.contract.d.ts +7 -0
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/prepush.contract.js +2 -2
- package/dist/contracts/providers.contract.d.ts +2 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +84 -86
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/safety.contract.d.ts +35 -0
- package/dist/contracts/safety.contract.d.ts.map +1 -0
- package/dist/contracts/safety.contract.js +32 -0
- package/dist/contracts/safety.contract.js.map +1 -0
- package/dist/contracts/sessions.contract.d.ts +73 -337
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +30 -32
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +139 -337
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +31 -3
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.d.ts +29 -20
- package/dist/contracts/usage.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.js +13 -0
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +8 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/conversation-ids.d.ts +2 -0
- package/dist/conversation-ids.d.ts.map +1 -1
- package/dist/conversation-ids.js +8 -0
- package/dist/conversation-ids.js.map +1 -1
- package/dist/credential-material.d.ts +1 -0
- package/dist/credential-material.d.ts.map +1 -1
- package/dist/credential-material.js +24 -2
- package/dist/credential-material.js.map +1 -1
- package/dist/definition.d.ts +51 -93
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +6 -28
- package/dist/definition.js.map +1 -1
- package/dist/events.d.ts +1831 -2285
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +155 -55
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +1 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +1959 -1903
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/ingress-protocol.d.ts +13 -1
- package/dist/ingress-protocol.d.ts.map +1 -1
- package/dist/ingress-protocol.js +19 -0
- package/dist/ingress-protocol.js.map +1 -1
- package/dist/mentions.d.ts +3 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +13 -0
- package/dist/mentions.js.map +1 -0
- package/dist/needs-action.d.ts +7 -0
- package/dist/needs-action.d.ts.map +1 -0
- package/dist/needs-action.js +3 -0
- package/dist/needs-action.js.map +1 -0
- package/dist/overlay-lint.d.ts +7 -0
- package/dist/overlay-lint.d.ts.map +1 -0
- package/dist/overlay-lint.js +68 -0
- package/dist/overlay-lint.js.map +1 -0
- package/dist/owner-ticket.d.ts +18 -0
- package/dist/owner-ticket.d.ts.map +1 -0
- package/dist/owner-ticket.js +51 -0
- package/dist/owner-ticket.js.map +1 -0
- package/dist/plan-pools.d.ts +12 -0
- package/dist/plan-pools.d.ts.map +1 -0
- package/dist/plan-pools.js +30 -0
- package/dist/plan-pools.js.map +1 -0
- package/dist/provider-specs.d.ts +228 -0
- package/dist/provider-specs.d.ts.map +1 -0
- package/dist/provider-specs.js +124 -0
- package/dist/provider-specs.js.map +1 -0
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +2 -1
- package/dist/quick-model.js.map +1 -1
- package/dist/runner-protocol.js +2 -2
- package/dist/runner-protocol.js.map +1 -1
- package/dist/runtime-state.d.ts +2 -2
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/safety-policy.d.ts +49 -0
- package/dist/safety-policy.d.ts.map +1 -0
- package/dist/safety-policy.js +52 -0
- package/dist/safety-policy.js.map +1 -0
- package/dist/schemas/agent.d.ts +21 -3
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +18 -8
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +19 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +15 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/approvals.d.ts +241 -0
- package/dist/schemas/approvals.d.ts.map +1 -0
- package/dist/schemas/approvals.js +63 -0
- package/dist/schemas/approvals.js.map +1 -0
- package/dist/schemas/automations.d.ts +5 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/ci.d.ts +5 -9
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +3 -10
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/computers.d.ts +31 -5
- package/dist/schemas/computers.d.ts.map +1 -1
- package/dist/schemas/computers.js +7 -5
- package/dist/schemas/computers.js.map +1 -1
- package/dist/schemas/environment.d.ts +11 -131
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +5 -42
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/git-history.d.ts +4 -0
- package/dist/schemas/git-history.d.ts.map +1 -1
- package/dist/schemas/git.d.ts +179 -0
- package/dist/schemas/git.d.ts.map +1 -1
- package/dist/schemas/git.js +39 -1
- package/dist/schemas/git.js.map +1 -1
- package/dist/schemas/panels.d.ts +21 -0
- package/dist/schemas/panels.d.ts.map +1 -1
- package/dist/schemas/panels.js +5 -0
- package/dist/schemas/panels.js.map +1 -1
- package/dist/schemas/plan-limits.d.ts +50 -3
- package/dist/schemas/plan-limits.d.ts.map +1 -1
- package/dist/schemas/plan-limits.js +11 -6
- package/dist/schemas/plan-limits.js.map +1 -1
- package/dist/schemas/provider-oauth.d.ts +6 -0
- package/dist/schemas/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.d.ts +2 -1
- package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.js +2 -1
- package/dist/schemas/provider-subscriptions.js.map +1 -1
- package/dist/schemas/settings.d.ts +29 -16
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +13 -20
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +3 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +5 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/workspace-repos.d.ts +14 -0
- package/dist/schemas/workspace-repos.d.ts.map +1 -1
- package/dist/schemas/workspace-repos.js +5 -0
- package/dist/schemas/workspace-repos.js.map +1 -1
- package/dist/schemas/workspace-tree.d.ts +1 -0
- package/dist/schemas/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace-tree.js +3 -0
- package/dist/schemas/workspace-tree.js.map +1 -1
- package/dist/transcript-fold.d.ts +31 -0
- package/dist/transcript-fold.d.ts.map +1 -0
- package/dist/transcript-fold.js +457 -0
- package/dist/transcript-fold.js.map +1 -0
- package/dist/workspace-state.d.ts +20 -3
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +22 -2
- package/dist/workspace-state.js.map +1 -1
- package/package.json +27 -5
- package/src/agent-catalog.test.ts +1 -1
- package/src/agent-catalog.ts +76 -555
- package/src/agent-run-model.test.ts +31 -15
- package/src/agent-run-model.ts +26 -12
- package/src/agent-runtimes.ts +419 -0
- package/src/approvals-execution.ts +96 -0
- package/src/arrival.ts +160 -0
- package/src/capability-ledger.test.ts +6 -4
- package/src/card-status.ts +93 -0
- package/src/chores/chores.ts +6 -3
- package/src/command-classes.test.ts +25 -0
- package/src/command-classes.ts +47 -5
- package/src/command-run.ts +78 -0
- package/src/contract-lock.test.ts +1 -1
- package/src/contract-lock.ts +2 -2
- package/src/contracts/agents.contract.ts +35 -2
- package/src/contracts/approvals.contract.ts +37 -0
- package/src/contracts/ci.contract.ts +0 -13
- package/src/contracts/git.contract.ts +28 -3
- package/src/contracts/keys.contract.ts +79 -0
- package/src/contracts/prepush.contract.ts +3 -3
- package/src/contracts/safety.contract.ts +48 -0
- package/src/contracts/usage.contract.ts +22 -0
- package/src/conversation-ids.test.ts +31 -1
- package/src/conversation-ids.ts +37 -3
- package/src/credential-material.test.ts +69 -1
- package/src/credential-material.ts +86 -5
- package/src/definition.ts +25 -53
- package/src/events.ts +374 -173
- package/src/history-state.ts +6 -0
- package/src/index.ts +24 -5
- package/src/ingress-protocol.ts +52 -1
- package/src/mentions.test.ts +22 -0
- package/src/mentions.ts +25 -0
- package/src/needs-action.ts +14 -0
- package/src/overlay-lint.test.ts +89 -0
- package/src/overlay-lint.ts +116 -0
- package/src/owner-ticket.test.ts +54 -0
- package/src/owner-ticket.ts +102 -0
- package/src/plan-pools.test.ts +71 -0
- package/src/plan-pools.ts +82 -0
- package/src/provider-specs.test.ts +130 -0
- package/src/provider-specs.ts +352 -0
- package/src/quick-model.ts +9 -5
- package/src/runner-protocol.ts +2 -2
- package/src/runtime-state.ts +6 -6
- package/src/safety-policy.ts +165 -0
- package/src/schemas/agent.ts +62 -17
- package/src/schemas/agents.ts +71 -0
- package/src/schemas/approvals.ts +174 -0
- package/src/schemas/ci.ts +19 -26
- package/src/schemas/computers.ts +52 -11
- package/src/schemas/environment.ts +21 -100
- package/src/schemas/git.ts +121 -6
- package/src/schemas/issues.ts +1 -1
- package/src/schemas/panels.ts +20 -0
- package/src/schemas/plan-limits.ts +58 -11
- package/src/schemas/provider-subscriptions.ts +7 -2
- package/src/schemas/settings.ts +67 -93
- package/src/schemas/system.ts +12 -1
- package/src/schemas/workspace-repos.ts +9 -0
- package/src/schemas/workspace-tree.ts +11 -0
- package/src/transcript-fold.test.ts +493 -0
- package/src/transcript-fold.ts +655 -0
- package/src/workspace-state.test.ts +77 -29
- package/src/workspace-state.ts +99 -31
- package/dist/contracts/drafts.contract.d.ts +0 -57
- package/dist/contracts/drafts.contract.d.ts.map +0 -1
- package/dist/contracts/drafts.contract.js +0 -32
- package/dist/contracts/drafts.contract.js.map +0 -1
- package/dist/definition-action.d.ts +0 -7
- package/dist/definition-action.d.ts.map +0 -1
- package/dist/definition-action.js +0 -3
- package/dist/definition-action.js.map +0 -1
- package/dist/publish-drafts.d.ts +0 -7
- package/dist/publish-drafts.d.ts.map +0 -1
- package/dist/publish-drafts.js +0 -20
- package/dist/publish-drafts.js.map +0 -1
- package/dist/schemas/drafts.d.ts +0 -84
- package/dist/schemas/drafts.d.ts.map +0 -1
- package/dist/schemas/drafts.js +0 -44
- package/dist/schemas/drafts.js.map +0 -1
- package/src/contracts/drafts.contract.ts +0 -35
- package/src/definition-action.ts +0 -8
- package/src/publish-drafts.ts +0 -59
- package/src/schemas/drafts.ts +0 -108
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* HOW AN APPROVED THING GETS DONE, the shared half, written once because the daemon acts on it, the app draws
|
|
2
|
+
* it, and the two have to agree on the same seconds.
|
|
3
|
+
*
|
|
4
|
+
* THERE IS NO EXECUTOR AUTOMATION, and its absence is the design. Publishing used to be a scheduled automation:
|
|
5
|
+
* a cron waking every few minutes, running a shell guard over the queue directory, almost always finding
|
|
6
|
+
* nothing, a job whose entire job was to ask "yet?" forever. It also made the approve button conditional on a
|
|
7
|
+
* row in a list nobody had asked for: delete the automation and approvals silently went nowhere, with the
|
|
8
|
+
* button still there and still saying yes. The daemon owns execution (approvals-executor.ts). It knows the
|
|
9
|
+
* moment an item comes due because it is the process that wrote it, so it sleeps until exactly then and costs
|
|
10
|
+
* nothing in between.
|
|
11
|
+
*
|
|
12
|
+
* APPROVAL IS NOT "DO IT NOW", IT IS "DO IT UNLESS I STOP YOU". A post is public and permanent the instant it
|
|
13
|
+
* lands, a booking is charged, and the gap between realising and clicking is about two seconds, so an approved
|
|
14
|
+
* item carrying no date of its own is dated HOLD into the future, and the queue counts it down in the open. The
|
|
15
|
+
* hold is stored as an ordinary scheduledAt rather than as a new state, which is what keeps it honest: it
|
|
16
|
+
* survives a restart, it reads as one number in the same place a scheduled item's date is already read, and
|
|
17
|
+
* calling it off is the "put it back in review" click that was there before any of this.
|
|
18
|
+
*
|
|
19
|
+
* A MINUTE, because that is the whole width of the decision: long enough to catch the wrong word you see the
|
|
20
|
+
* moment the row stops being a form, short enough that approving something still feels like doing it. */
|
|
21
|
+
export const APPROVAL_HOLD_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
/* WHO CAN BE SENT BY CODE ALONE. A connector reached through a real API with a stored credential is a request
|
|
24
|
+
* the daemon can make itself, no model, no browser, no turn, and it either got a 200 or it did not. A
|
|
25
|
+
* connector that IS a logged-in browser session (reddit, x) has no such door: posting there means driving a
|
|
26
|
+
* page whose markup moves under you, past dialogs and rate screens nobody can enumerate in advance, which is
|
|
27
|
+
* precisely the work an agent turn exists to absorb.
|
|
28
|
+
*
|
|
29
|
+
* So the split follows what the platform actually offers rather than what would be cheaper, and it is stated
|
|
30
|
+
* here rather than guessed at a call site, because being wrong in the optimistic direction means a post that
|
|
31
|
+
* silently never goes out. A platform absent from this set is published by an agent turn, which always works
|
|
32
|
+
* and merely costs more. */
|
|
33
|
+
export const DIRECT_PUBLISH_PLATFORMS: ReadonlySet<string> = new Set(["discord"]);
|
|
34
|
+
|
|
35
|
+
// The workspace-relative directory both prompts below name, spelled once.
|
|
36
|
+
const APPROVALS_DIR = ".intentic/config/approvals";
|
|
37
|
+
|
|
38
|
+
// The status bookkeeping every executing turn is told, identical across kinds so the file the queue reads back
|
|
39
|
+
// is the same shape whoever wrote it.
|
|
40
|
+
const WRITE_BACK = [
|
|
41
|
+
`Before you act on one, set its "status" to "running" and "startedAt" to the epoch ms, so a turn that dies`,
|
|
42
|
+
`here cannot do it twice. When it is done, set "status":"done" plus "finishedAt" (epoch ms) and, where`,
|
|
43
|
+
`there is one, "result": the post's URL, a confirmation number, whatever a person would want to go and`,
|
|
44
|
+
`look at. If it failed, set "status":"failed" plus an "error" saying what went wrong in plain words the`,
|
|
45
|
+
`owner can act on, then move to the next one instead of retrying in a loop.`,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/* WHAT THE PUBLISH TURN IS TOLD, for the posts no API can carry. It NAMES the files rather than saying "go and
|
|
49
|
+
* look": the daemon has already decided what is due, and a turn that re-derives that decision can disagree
|
|
50
|
+
* with it, sending something the owner pulled back a second ago, or skipping something it judged not ready.
|
|
51
|
+
* The turn's job is the part only it can do, which is working the platform's own UI.
|
|
52
|
+
*
|
|
53
|
+
* It still writes the outcome back into the file, because the file is where the queue reads it, and a post
|
|
54
|
+
* that went out without saying so is a post the owner sends twice. */
|
|
55
|
+
export const publishTurnPrompt = (posts: readonly { readonly id: string; readonly platform: string }[]): string =>
|
|
56
|
+
[
|
|
57
|
+
`Publish these approved posts, which are due now. They live in ${APPROVALS_DIR}/, one JSON file each:`,
|
|
58
|
+
``,
|
|
59
|
+
...posts.map((post) => `- ${post.id}.json (${post.platform})`),
|
|
60
|
+
``,
|
|
61
|
+
`Take them ONE AT A TIME, and for each:`,
|
|
62
|
+
`1. Read the file. ${WRITE_BACK[0]}`,
|
|
63
|
+
` ${WRITE_BACK[1]}`,
|
|
64
|
+
`2. Post exactly its "content": with its "title", "target" and "media", using that platform's skill.`,
|
|
65
|
+
` A "target" that is a URL means this post is a REPLY to whatever is at it: open that exact URL and`,
|
|
66
|
+
` reply where it lands. On reddit a comment permalink (.../comments/<post>/<slug>/<comment>/) has to`,
|
|
67
|
+
` nest under that comment rather than becoming a new top-level comment on the thread.`,
|
|
68
|
+
`3. ${WRITE_BACK[2]} ${WRITE_BACK[3]}`,
|
|
69
|
+
` ${WRITE_BACK[4]}`,
|
|
70
|
+
``,
|
|
71
|
+
`Never rewrite the content: the owner approved these exact words. Never touch a file not listed above.`,
|
|
72
|
+
].join(`\n`);
|
|
73
|
+
|
|
74
|
+
/* WHAT THE ACTION TURN IS TOLD. The same discipline as a publish turn, named files, status written before and
|
|
75
|
+
* after, with the work itself coming from the file: the agent that proposed the action wrote its own
|
|
76
|
+
* instructions for this moment, knowing this turn would arrive with none of the conversation. The owner's yes
|
|
77
|
+
* covered what `summary` and `details` said, so the instructions are carried out as written and nothing beyond
|
|
78
|
+
* them is done; an action that turns out to need more than it said is failed with that sentence, not
|
|
79
|
+
* improvised. */
|
|
80
|
+
export const actionTurnPrompt = (actions: readonly { readonly id: string; readonly summary: string }[]): string =>
|
|
81
|
+
[
|
|
82
|
+
`Carry out these approved actions, which are due now. They live in ${APPROVALS_DIR}/, one JSON file each:`,
|
|
83
|
+
``,
|
|
84
|
+
...actions.map((action) => `- ${action.id}.json: ${action.summary}`),
|
|
85
|
+
``,
|
|
86
|
+
`Take them ONE AT A TIME, and for each:`,
|
|
87
|
+
`1. Read the file. ${WRITE_BACK[0]}`,
|
|
88
|
+
` ${WRITE_BACK[1]}`,
|
|
89
|
+
`2. Do exactly what its "instructions" say. The owner approved what "summary" and "details" describe and`,
|
|
90
|
+
` nothing more: if carrying it out would need something they do not say, stop and fail it with that`,
|
|
91
|
+
` reason rather than improvising.`,
|
|
92
|
+
`3. ${WRITE_BACK[2]} ${WRITE_BACK[3]}`,
|
|
93
|
+
` ${WRITE_BACK[4]}`,
|
|
94
|
+
``,
|
|
95
|
+
`Never touch a file not listed above.`,
|
|
96
|
+
].join(`\n`);
|
package/src/arrival.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { NeedsActionSchema } from "./needs-action.js";
|
|
3
|
+
|
|
4
|
+
/* AN ARRIVAL: something coming INTO this sandbox, whatever it came from and whatever it is made of.
|
|
5
|
+
*
|
|
6
|
+
* There used to be three of these, each with its own schemas, its own routes and its own card, and the split
|
|
7
|
+
* was by ARTIFACT: a `sandbox.toml` had one surface, an environment bundle a second, a foreign assistant's
|
|
8
|
+
* home directory a third. That is the wrong axis. The owner's question is never "which of your three import
|
|
9
|
+
* features is this" — it is "I have a thing, take what is safe from it" — and the three artifacts answer it
|
|
10
|
+
* with the same four moves: read the thing, show what would land, take the ticked rows, say what is left.
|
|
11
|
+
*
|
|
12
|
+
* So there is ONE pipeline, and the artifact is a parser:
|
|
13
|
+
*
|
|
14
|
+
* source → plan (items the owner ticks, refused lines, needsAction said BEFORE anything writes)
|
|
15
|
+
* → apply (the ticked ids, against a plan RE-DERIVED from the held bytes)
|
|
16
|
+
* → report (what landed, what did not and why, what still needs a person)
|
|
17
|
+
*
|
|
18
|
+
* WHAT THE MERGE FIXED, beyond three of everything: the bundle door was the only one that WROTE ON FILE PICK.
|
|
19
|
+
* It is also the most destructive of the three — a bundle lands over a workspace rather than beside it — so
|
|
20
|
+
* the one arrival most deserving of a preview was the one without it. Sharing the pipeline is what gave it
|
|
21
|
+
* one, and what makes "untick the 6 GB monorepo" a thing an owner can say to a bundle at all.
|
|
22
|
+
*
|
|
23
|
+
* THE OUTBOUND HALF IS NOT HERE. Deriving a definition, packing a bundle and publishing the workspace repo
|
|
24
|
+
* live on definition.ts and schemas/environment.ts, because they answer the opposite question and share no
|
|
25
|
+
* shape with this one. */
|
|
26
|
+
|
|
27
|
+
// Which of the four things this is. The two assistants are separate members rather than one "foreign": the
|
|
28
|
+
// checklist badges the source by name, and an owner who packed a Hermes folder should read the word Hermes.
|
|
29
|
+
export const ArrivalSourceSchema = z.enum(["definition", "bundle", "hermes", "openclaw"]);
|
|
30
|
+
export type ArrivalSource = z.infer<typeof ArrivalSourceSchema>;
|
|
31
|
+
|
|
32
|
+
// The subset a foreign assistant's home directory can be. Narrower than ArrivalSource on purpose: a connected
|
|
33
|
+
// computer is scanned for these two and never for a bundle, which is a file, not a setup.
|
|
34
|
+
export const AssistantSourceSchema = z.enum(["hermes", "openclaw"]);
|
|
35
|
+
export type AssistantSource = z.infer<typeof AssistantSourceSchema>;
|
|
36
|
+
|
|
37
|
+
/* WHAT AN ITEM BECOMES HERE, not what it was there. The apply loop dispatches on this and the checklist
|
|
38
|
+
* groups by it, so an owner reads "3 skills, 2 connections" rather than a foreign directory listing or a tar
|
|
39
|
+
* table of contents. One enum across all four sources, which is what makes the union honest: a repository is
|
|
40
|
+
* a `repo` whether it arrived as a remote to clone (definition) or as a git dir in a tar (bundle), and the
|
|
41
|
+
* owner ticking it means the same thing either way. */
|
|
42
|
+
export const ArrivalGroupSchema = z.enum([
|
|
43
|
+
// The /work tree as a whole, cloned from the remote a definition's `[workspace]` names.
|
|
44
|
+
"workspace",
|
|
45
|
+
// One repository: cloned from its remote, or unpacked with its real git dir from a bundle.
|
|
46
|
+
"repo",
|
|
47
|
+
// Loose files landing in /work: a bundle's workspace bytes, an assistant's notes folder.
|
|
48
|
+
"files",
|
|
49
|
+
// Transcripts, checkpoint timelines, ledgers. Bundle-only — nothing else can reference them.
|
|
50
|
+
"history",
|
|
51
|
+
// The overlay Dockerfile.
|
|
52
|
+
"environment",
|
|
53
|
+
// One connection, landing unauthenticated unless its secret travelled.
|
|
54
|
+
"capability",
|
|
55
|
+
// The agent settings that differ from their defaults.
|
|
56
|
+
"settings",
|
|
57
|
+
// A merge into CLAUDE.md / AGENTS.md.
|
|
58
|
+
"memory",
|
|
59
|
+
"skill",
|
|
60
|
+
"automation",
|
|
61
|
+
// One credential VALUE, and the only group gated behind the apply's second consent.
|
|
62
|
+
"secret",
|
|
63
|
+
]);
|
|
64
|
+
export type ArrivalGroup = z.infer<typeof ArrivalGroupSchema>;
|
|
65
|
+
|
|
66
|
+
/* ONE ROW OF THE CHECKLIST. Two flags rather than one, because the four sources fail an item for two
|
|
67
|
+
* genuinely different reasons and collapsing them would lose the one the owner can act on:
|
|
68
|
+
*
|
|
69
|
+
* `applicable: false` the target already holds this, so it CANNOT be ticked. A definition and a bundle
|
|
70
|
+
* land beside what is there, never over it, so an existing repo directory or
|
|
71
|
+
* capability id greys its row and prints the reason.
|
|
72
|
+
* `recommended: false` it can be ticked and the reader should look first. An adapter's judgment about a
|
|
73
|
+
* server URL pointing at the old machine, or an env key that reads as tuning.
|
|
74
|
+
*
|
|
75
|
+
* A source that has no use for one of them fills it constantly: a definition recommends everything it can
|
|
76
|
+
* apply, an assistant adapter refuses inside its own walk rather than emitting inapplicable rows. */
|
|
77
|
+
export const ArrivalItemSchema = z.object({
|
|
78
|
+
/* Deterministic and derived from the artifact ("repo:intentic", "skill:weather", "bundle:history"), which
|
|
79
|
+
* is the whole reason the apply may re-derive: the ids the owner ticked name the same items in the
|
|
80
|
+
* second derivation as in the one the browser rendered. */
|
|
81
|
+
id: z.string(),
|
|
82
|
+
group: ArrivalGroupSchema,
|
|
83
|
+
// The checklist line, in plain words: "Repository intentic", "Skill, weather", "Workspace files".
|
|
84
|
+
label: z.string(),
|
|
85
|
+
detail: z.string().optional(),
|
|
86
|
+
applicable: z.boolean(),
|
|
87
|
+
// Why it cannot be ticked. Present exactly when `applicable` is false.
|
|
88
|
+
reason: z.string().optional(),
|
|
89
|
+
recommended: z.boolean(),
|
|
90
|
+
/* Names of the secrets this row would store — never values. Non-empty rows only move when the apply
|
|
91
|
+
* carries `includeSecrets`; the report names what stayed behind rather than pretending it landed. */
|
|
92
|
+
secrets: z.array(z.string()),
|
|
93
|
+
});
|
|
94
|
+
export type ArrivalItem = z.infer<typeof ArrivalItemSchema>;
|
|
95
|
+
|
|
96
|
+
export const ArrivalPlanSchema = z.object({
|
|
97
|
+
source: ArrivalSourceSchema,
|
|
98
|
+
/* Names the held artifact for the apply call. Minted per plan; a new plan replaces the held one, because
|
|
99
|
+
* an owner changing their mind is the ordinary case and not a conflict. */
|
|
100
|
+
token: z.string(),
|
|
101
|
+
// What the artifact calls itself: a definition's `name`, a bundle's source sandbox. For the reader only.
|
|
102
|
+
name: z.string().optional(),
|
|
103
|
+
items: z.array(ArrivalItemSchema),
|
|
104
|
+
/* Whether this artifact holds credential VALUES at all, which is what decides whether the apply's second
|
|
105
|
+
* consent is even a question. A definition is false by construction (it carries names, never values); a
|
|
106
|
+
* bundle is whatever its owner chose at export; an assistant's home directory is true whenever any row
|
|
107
|
+
* names a secret. The card asks with a toggle only when this is true, so the ordinary arrival is not made
|
|
108
|
+
* to answer a question about credentials that do not exist. */
|
|
109
|
+
carriesSecrets: z.boolean(),
|
|
110
|
+
// What the reader saw and will not offer at all: sessions, logs, pairing state, a tar entry this daemon
|
|
111
|
+
// refuses to write. Listed rather than silent.
|
|
112
|
+
refused: z.array(z.string()),
|
|
113
|
+
// What is already known not to move mechanically, surfaced at PREVIEW time so the owner ticks with open
|
|
114
|
+
// eyes and again on the report.
|
|
115
|
+
needsAction: z.array(NeedsActionSchema),
|
|
116
|
+
});
|
|
117
|
+
export type ArrivalPlan = z.infer<typeof ArrivalPlanSchema>;
|
|
118
|
+
|
|
119
|
+
export const ArrivalApplySchema = z.object({
|
|
120
|
+
token: z.string(),
|
|
121
|
+
// The ticked ids. Ids the re-derived plan does not contain are ignored rather than erroring: the artifact
|
|
122
|
+
// is the truth, and a stale checklist must not block the items that still exist.
|
|
123
|
+
items: z.array(z.string()),
|
|
124
|
+
/* The owner's explicit consent to move credential VALUES, asked once, on the inbound side, for every
|
|
125
|
+
* source. It used to be asked at EXPORT time for a bundle and at APPLY time for an assistant, which is
|
|
126
|
+
* the same question in two places with two different answers about who is consenting to what. */
|
|
127
|
+
includeSecrets: z.boolean(),
|
|
128
|
+
});
|
|
129
|
+
export type ArrivalApply = z.infer<typeof ArrivalApplySchema>;
|
|
130
|
+
|
|
131
|
+
export const ArrivalReportSchema = z.object({
|
|
132
|
+
applied: z.array(z.object({ id: z.string(), group: ArrivalGroupSchema, label: z.string() })),
|
|
133
|
+
// Ticked and did not land, each with its reason. Distinct from `refused` and from inapplicable rows,
|
|
134
|
+
// which were never attempted.
|
|
135
|
+
failed: z.array(z.object({ id: z.string(), label: z.string(), error: z.string() })),
|
|
136
|
+
refused: z.array(z.string()),
|
|
137
|
+
needsAction: z.array(NeedsActionSchema),
|
|
138
|
+
});
|
|
139
|
+
export type ArrivalReport = z.infer<typeof ArrivalReportSchema>;
|
|
140
|
+
|
|
141
|
+
/* ONE OF THE OWNER'S OWN COMPUTERS, as an arrival source that needs no packing at all: the daemon walks the
|
|
142
|
+
* machine's home folder over the socket it already holds. Read on the card's first render for every enrolled
|
|
143
|
+
* machine, so the offer appears before the owner has read a single instruction.
|
|
144
|
+
*
|
|
145
|
+
* `found` absent means "connected, and nothing to bring in from here", which is a real answer worth rendering
|
|
146
|
+
* quietly rather than an error: the machine may simply not be the one the assistant runs on. */
|
|
147
|
+
export const ArrivalHostSchema = z.object({
|
|
148
|
+
id: z.string(),
|
|
149
|
+
online: z.boolean(),
|
|
150
|
+
found: AssistantSourceSchema.optional(),
|
|
151
|
+
// Why this machine cannot be read right now, when it cannot: asleep, or its own refusal, in its words.
|
|
152
|
+
detail: z.string().optional(),
|
|
153
|
+
});
|
|
154
|
+
export type ArrivalHost = z.infer<typeof ArrivalHostSchema>;
|
|
155
|
+
export const ArrivalHostsSchema = z.object({ hosts: z.array(ArrivalHostSchema) });
|
|
156
|
+
|
|
157
|
+
// Read a setup off a connected computer instead of an upload. Answers with a plan exactly as the upload door
|
|
158
|
+
// does; everything after this point is identical whichever door the arrival came through.
|
|
159
|
+
export const ArrivalScanSchema = z.object({ host: z.string().min(1) });
|
|
160
|
+
export type ArrivalScan = z.infer<typeof ArrivalScanSchema>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { capabilitiesOf, limitationsOf } from "./agent-catalog.js";
|
|
3
|
+
import type { AgentCapabilities } from "./agent-runtimes.js";
|
|
3
4
|
|
|
4
5
|
/* THE CAPABILITY LEDGER, which claims in the record are backed by machinery, and which by prose.
|
|
5
6
|
*
|
|
@@ -118,9 +119,10 @@ describe("a descriptive claim reaches the user", () => {
|
|
|
118
119
|
// A boolean's floor is `false` and needs no entry; every other descriptive field has to name its own.
|
|
119
120
|
// Asserted against the KEYS of DIMINISHED so the failure prints the floors that do exist, which is what
|
|
120
121
|
// tells "this field was never added" apart from "this field was renamed and its entry left behind".
|
|
121
|
-
expect(
|
|
122
|
-
field,
|
|
123
|
-
|
|
122
|
+
expect(
|
|
123
|
+
boolean ? [field] : Object.keys(DIMINISHED),
|
|
124
|
+
`${field} is descriptive and not a boolean: add its weakest value to DIMINISHED above`,
|
|
125
|
+
).toContain(field);
|
|
124
126
|
const floor = boolean ? false : DIMINISHED[field];
|
|
125
127
|
|
|
126
128
|
const lacking: AgentCapabilities = { ...CEILING, [field]: floor };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { CardField, TranscriptCards, TranscriptRow } from "./events.js";
|
|
2
|
+
import type { AgentReply } from "./schemas/plan-limits.js";
|
|
3
|
+
|
|
4
|
+
/* HOW A REPLY SETTLES THE CARD IT ANSWERS, the one derivation, used by the fold when the `resolved` frame lands
|
|
5
|
+
* (transcript-fold.ts) and by a chat freezing its own card the instant its answer is accepted, before that
|
|
6
|
+
* frame comes back. Two callers, one rule, which is what keeps "answered" looking the same on the window that
|
|
7
|
+
* clicked and on every window that only watched.
|
|
8
|
+
*
|
|
9
|
+
* No reply is nobody answering: the turn was stopped, or died under the card, which is not a decision, so every
|
|
10
|
+
* card reads `cancelled`. A reply of the wrong kind cannot reach a card of another, the requestId is what
|
|
11
|
+
* matched it, and reads as unanswered rather than inventing a decision. */
|
|
12
|
+
type Cards = { -readonly [K in CardField]?: TranscriptRow[K] };
|
|
13
|
+
|
|
14
|
+
export const settledCards = (cards: TranscriptCards, reply: AgentReply | undefined): TranscriptCards => {
|
|
15
|
+
const out: Cards = {};
|
|
16
|
+
const { plan, question, permission, browserHelp, terminalHelp, serviceOffer, capabilityOffer, paymentOffer } = cards;
|
|
17
|
+
if (plan !== undefined) {
|
|
18
|
+
out.plan = { ...plan, status: reply?.kind !== "plan" ? "cancelled" : reply.approve ? "approved" : "rejected" };
|
|
19
|
+
}
|
|
20
|
+
if (question !== undefined) {
|
|
21
|
+
const answers = reply?.kind === "question" ? reply.answers : undefined;
|
|
22
|
+
out.question = {
|
|
23
|
+
...question,
|
|
24
|
+
status: reply?.kind === "question" && reply.cancelled !== true ? "answered" : "cancelled",
|
|
25
|
+
...(answers === undefined ? {} : { answers }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (permission !== undefined) {
|
|
29
|
+
out.permission = {
|
|
30
|
+
...permission,
|
|
31
|
+
status:
|
|
32
|
+
reply?.kind !== "permission"
|
|
33
|
+
? "cancelled"
|
|
34
|
+
: reply.decision === "deny"
|
|
35
|
+
? "denied"
|
|
36
|
+
: reply.decision === "always"
|
|
37
|
+
? "always"
|
|
38
|
+
: "allowed",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (browserHelp !== undefined) {
|
|
42
|
+
out.browserHelp = { ...browserHelp, status: reply?.kind !== "browser_help" ? "cancelled" : reply.helped ? "helped" : "declined" };
|
|
43
|
+
}
|
|
44
|
+
if (terminalHelp !== undefined) {
|
|
45
|
+
out.terminalHelp = { ...terminalHelp, status: reply?.kind !== "terminal_help" ? "cancelled" : reply.helped ? "helped" : "declined" };
|
|
46
|
+
}
|
|
47
|
+
if (serviceOffer !== undefined) {
|
|
48
|
+
out.serviceOffer = { ...serviceOffer, status: reply?.kind !== "service_offer" ? "cancelled" : reply.approve ? "approved" : "skipped" };
|
|
49
|
+
}
|
|
50
|
+
// A yes settles the DECISION, not the ask: the owner is now setting the capability up, so the card moves to
|
|
51
|
+
// `connecting` and stays there until the capability_outcome frame says how the setup ended.
|
|
52
|
+
if (capabilityOffer !== undefined) {
|
|
53
|
+
out.capabilityOffer = {
|
|
54
|
+
...capabilityOffer,
|
|
55
|
+
status: reply?.kind !== "capability_offer" ? "cancelled" : reply.connect ? "connecting" : "skipped",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// A yes settles the decision; whether the money actually moved is the payment_receipt frame's to say.
|
|
59
|
+
if (paymentOffer !== undefined) {
|
|
60
|
+
out.paymentOffer = { ...paymentOffer, status: reply?.kind !== "payment_offer" ? "cancelled" : reply.approve ? "approved" : "skipped" };
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* The same cards with every one still `pending` frozen as `cancelled`: the turn ended out from under the ask
|
|
66
|
+
* (a Stop, a death, a failure) and nobody decided anything. Returns the SAME object when nothing was pending,
|
|
67
|
+
* so a caller can tell a row that changed from one that did not. */
|
|
68
|
+
export const cancelledCards = (cards: TranscriptCards): TranscriptCards => {
|
|
69
|
+
const out: Cards = {};
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const field of [
|
|
72
|
+
"plan",
|
|
73
|
+
"question",
|
|
74
|
+
"permission",
|
|
75
|
+
"browserHelp",
|
|
76
|
+
"terminalHelp",
|
|
77
|
+
"serviceOffer",
|
|
78
|
+
"capabilityOffer",
|
|
79
|
+
"paymentOffer",
|
|
80
|
+
] as const) {
|
|
81
|
+
const card = cards[field];
|
|
82
|
+
if (card === undefined) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (card.status === "pending") {
|
|
86
|
+
changed = true;
|
|
87
|
+
(out as Record<CardField, unknown>)[field] = { ...card, status: "cancelled" };
|
|
88
|
+
} else {
|
|
89
|
+
(out as Record<CardField, unknown>)[field] = card;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return changed ? out : cards;
|
|
93
|
+
};
|
package/src/chores/chores.ts
CHANGED
|
@@ -460,9 +460,12 @@ const testStrength: Chore = {
|
|
|
460
460
|
criterion: `Stryker's mutation score for the repo is under ${MUTATION_FLOOR}%.`,
|
|
461
461
|
stance: `act`,
|
|
462
462
|
needs: [`mutation`],
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
463
|
+
/* Weekly. This was quarterly on the reasoning that a mutation score moves when tests are rewritten and that
|
|
464
|
+
* is not a weekly event; on 2026-08-31 agents rewrote about 180 test files in an afternoon, and the score is
|
|
465
|
+
* exactly the number that should have said so. The probe is still the most expensive one here, and
|
|
466
|
+
* `--incremental` is what makes a weekly cadence affordable: the first run costs a full run, every one after it
|
|
467
|
+
* costs the mutants whose code or tests changed. */
|
|
468
|
+
cadenceMs: 7 * DAY_MS,
|
|
466
469
|
assess: (context) => {
|
|
467
470
|
const facts = factsOf(context, `mutation`);
|
|
468
471
|
if (facts === undefined || facts.mutation.score >= MUTATION_FLOOR) {
|
|
@@ -198,6 +198,31 @@ describe("secrets.access", () => {
|
|
|
198
198
|
}
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
+
/* A SEARCH PATTERN IS NOT A PATH, and the escaped dot is why this needed saying: `process.env` is excluded
|
|
202
|
+
* by the dotenv pattern's own lookbehind, and `process\.env` — the same thing typed into a grep — walked
|
|
203
|
+
* straight past it, because the character before `.env` is then a backslash rather than the `s`. Grepping
|
|
204
|
+
* this workspace for its own env reads was one of the commonest cards the class raised. */
|
|
205
|
+
test("a credential-shaped name inside a regex is a pattern, not a file", () => {
|
|
206
|
+
for (const command of [
|
|
207
|
+
String.raw`rg -n 'process\.env\.(INTENTIC_[A-Z]+|GITHUB_[A-Z]+)\b' --type ts .`,
|
|
208
|
+
String.raw`rg -o 'process\.env\.\w+' . | sort -u`,
|
|
209
|
+
String.raw`grep -rn '\.npmrc' .`,
|
|
210
|
+
String.raw`rg '\.ssh/id_ed25519' -l`,
|
|
211
|
+
String.raw`rg -n '\.env\b' -g '!*.md' .`,
|
|
212
|
+
]) {
|
|
213
|
+
expect(classifyCommand(command), command).not.toContain("secrets.access");
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/* THE ONE PATH THAT SPELLS `\.` AND IS A PATH. The machine agent's shell runs on somebody's Windows laptop,
|
|
218
|
+
* where the backslash is the separator, so the rule above must not read a real credential read as a regex:
|
|
219
|
+
* a separator is followed by a path segment, an escape by the character it escapes. */
|
|
220
|
+
test("a windows path keeps the class", () => {
|
|
221
|
+
for (const command of [String.raw`type C:\Users\me\.env`, String.raw`copy %USERPROFILE%\.ssh\id_rsa \tmp`]) {
|
|
222
|
+
expect(classifyCommand(command), command).toContain("secrets.access");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
201
226
|
/* Carrying a reference IS reading the credential: it becomes the value on the way into the process, so a
|
|
202
227
|
* command holding one belongs in this class however it is spelled. Otherwise the outside-content floor in
|
|
203
228
|
* actions.ts is bypassed by the shorter route to the same place, writing `{{secret:X}}` into a curl rather
|
package/src/command-classes.ts
CHANGED
|
@@ -20,6 +20,14 @@ import { type CommandClass, CommandClassSchema } from "./schemas/agent.js";
|
|
|
20
20
|
* boundary, the boundaries are structural and elsewhere: the container, the isolated worktree, the land gate,
|
|
21
21
|
* an automation's tool allowlist, and, on somebody's own computer, the scope switches enforced there.
|
|
22
22
|
*
|
|
23
|
+
* WHICH IS WHY THIS NO LONGER DECIDES ANYTHING. A match used to BE the verdict: whatever fired here became the
|
|
24
|
+
* permission card, so `echo "rm -rf /"`, `rg 'rm -rf'` and an actual delete were one question with one answer.
|
|
25
|
+
* Now a match only means A JUDGE SHOULD LOOK (safety-policy.ts argues the move at length, guard/command-gate.ts
|
|
26
|
+
* implements it), and the judge reads the owner's written policy plus what the daemon knows about the turn.
|
|
27
|
+
* That changes what these patterns should optimise for: being OVER-inclusive is close to free, because a false
|
|
28
|
+
* positive now costs one model call rather than one interruption, and a miss still costs everything. Anyone
|
|
29
|
+
* tuning a pattern below should widen rather than narrow it.
|
|
30
|
+
*
|
|
23
31
|
* Matching is deliberately UNANCHORED, substrings, not line starts. Another PreToolUse hook may have rewrapped
|
|
24
32
|
* the command by the time this reads it (agent-terminals.ts wraps every Bash call in bin/tmux-run), and the
|
|
25
33
|
* agent's own line survives verbatim inside that wrapper. Nothing the wrapper adds is in any class below.
|
|
@@ -414,6 +422,36 @@ const enclosingPath = (command: string, span: CommandSpan): string => {
|
|
|
414
422
|
.replace(/^[@<>=]+/, "");
|
|
415
423
|
};
|
|
416
424
|
|
|
425
|
+
/* A WORD THAT IS A PATTERN RATHER THAN A PATH, dropped before the table's guess about a FILE is believed at all.
|
|
426
|
+
*
|
|
427
|
+
* The table reads shell text looking for filenames, and a search command carries something that looks exactly
|
|
428
|
+
* like one and is not: `rg 'process\.env\.(INTENTIC_[A-Z]+)' --type ts .` names no file and opens nothing, and
|
|
429
|
+
* it earned a card reading "this command would read credential material" over a grep of this workspace's own
|
|
430
|
+
* source. The `.env` in it survives the dotenv pattern's `process.env` exclusion for one reason: the lookbehind
|
|
431
|
+
* sees the REGEX'S BACKSLASH rather than the `s` of `process`, and a backslash is neither a word character nor a
|
|
432
|
+
* dot. Every credential-shaped name has the same hole — `rg '\.npmrc'`, `rg '\.ssh/id_ed25519'` — so it is fixed
|
|
433
|
+
* once here rather than seven times in the table.
|
|
434
|
+
*
|
|
435
|
+
* THE ESCAPED DOT IS THE TELL. `\.` is how a regex spells a literal dot, and a POSIX path never needs it. The
|
|
436
|
+
* one thing that spells `\.` and IS a path is Windows (`type C:\Users\me\.env`), which the machine agent's shell
|
|
437
|
+
* really does see — and there the other backslashes are SEPARATORS, each followed by a path segment rather than
|
|
438
|
+
* by the character it escapes. That is the whole discrimination.
|
|
439
|
+
*
|
|
440
|
+
* A CHARACTER CLASS and a CLASS ESCAPE are the other two tells, and both are nearly free: `[…]` is legal in a
|
|
441
|
+
* filename and never in one anybody writes, and `\w`, `\d`, `\b` mean nothing to a shell. The class escapes are
|
|
442
|
+
* matched only where a word character does NOT follow, which is what keeps `\dev` and `\swap` (Windows
|
|
443
|
+
* directories) out of them. The word edges (WORD_EDGE) already cut a word at the `(`, `|` and quotes carrying
|
|
444
|
+
* the rest of a regex's syntax, so these are what is left of it by the time a word reaches here.
|
|
445
|
+
*
|
|
446
|
+
* Judged on the ENCLOSING WORD, the same word the fact-check would have asked the filesystem about, so a
|
|
447
|
+
* pattern and a path are told apart once and both consults see the same answer. */
|
|
448
|
+
const CHARACTER_CLASS = /\[[^\]]*\]/;
|
|
449
|
+
const CLASS_ESCAPE = /\\[wdsbWDSB](?!\w)/;
|
|
450
|
+
const ESCAPED_DOT = /\\\./;
|
|
451
|
+
const PATH_SEPARATOR = /\\\w/;
|
|
452
|
+
const namesAPattern = (word: string): boolean =>
|
|
453
|
+
CHARACTER_CLASS.test(word) || CLASS_ESCAPE.test(word) || (ESCAPED_DOT.test(word) && !PATH_SEPARATOR.test(word));
|
|
454
|
+
|
|
417
455
|
/* WHERE A COMMAND READS CREDENTIAL MATERIAL: every secret reference in it, plus every credential-shaped path the
|
|
418
456
|
* context did not positively clear.
|
|
419
457
|
*
|
|
@@ -422,7 +460,10 @@ const enclosingPath = (command: string, span: CommandSpan): string => {
|
|
|
422
460
|
* class would evaporate on every caller without a filesystem. */
|
|
423
461
|
const credentialReads = (command: string, context: CommandContext | undefined): CommandSpan[] => [
|
|
424
462
|
...spansOf(SECRET_REFERENCES_G, command),
|
|
425
|
-
...spansOf(CREDENTIAL_PATHS_G, command).filter((span) =>
|
|
463
|
+
...spansOf(CREDENTIAL_PATHS_G, command).filter((span) => {
|
|
464
|
+
const word = enclosingPath(command, span);
|
|
465
|
+
return !namesAPattern(word) && context?.holdsSecret?.(word) !== false;
|
|
466
|
+
}),
|
|
426
467
|
];
|
|
427
468
|
|
|
428
469
|
// WHERE each class fires, one entry per class. Empty ⇒ the command is not in it, so membership and evidence are
|
|
@@ -462,7 +503,8 @@ export const COMMAND_CLASS_LABELS: Readonly<Record<CommandClass, string>> = {
|
|
|
462
503
|
"network.outbound": "send a request out to the internet",
|
|
463
504
|
};
|
|
464
505
|
|
|
465
|
-
/*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
|
|
506
|
+
/* No verdict set lives here any more. Which classes are worth stopping for is a POLICY question now, and it is
|
|
507
|
+
* answered in two places that are honest about being different: safety-policy.ts's HARD_RULE_CLASSES for the
|
|
508
|
+
* one thing nothing recovers, and the owner's own written policy for everything else. The machine agent keeps
|
|
509
|
+
* its own set beside its scope switches (machine/src/computer/tools/shell.ts), because "which commands need
|
|
510
|
+
* the destructive switch" is a question about that capability card rather than about this catalog. */
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CommandRun } from "./schemas/ci.js";
|
|
2
|
+
|
|
3
|
+
/* THE HEADING OVER A SETTLED RUN, three or four words, because it is read at a glance and from across a view
|
|
4
|
+
* the user may have walked back into, and because it is read in TWO places that must agree: the card the app
|
|
5
|
+
* floats over the workspace, and the notification the daemon sends to a phone when the user is not there to
|
|
6
|
+
* see the card. Two copies of these words drifted once ("Checks failed" on the card, "Check failed" on the
|
|
7
|
+
* phone), which is the whole reason the sentence lives in the package both sides already share.
|
|
8
|
+
*
|
|
9
|
+
* `subject` is what ran, in the words the button used: "Checks" for the pre-push check, "Push" for the push,
|
|
10
|
+
* and the verb the user actually clicked ("Publish", "Sync") where the flow knows it. `passed` is included
|
|
11
|
+
* for totality; no card is raised for it, a pass sends the push and says so where the click was. */
|
|
12
|
+
export interface FollowRunOptions<R extends CommandRun> {
|
|
13
|
+
// The wait between "is it done yet" questions.
|
|
14
|
+
readonly intervalMs: number;
|
|
15
|
+
// Stop following: the caller has moved on. Resolves undefined rather than throwing, the run itself is
|
|
16
|
+
// still going wherever it was, and nobody here has anything to report about it.
|
|
17
|
+
readonly signal?: AbortSignal | undefined;
|
|
18
|
+
// Every state read, settled or not, for a surface that draws progress (the terminal to open, the command).
|
|
19
|
+
readonly onState?: ((run: R) => void) | undefined;
|
|
20
|
+
// A poll that could not be read. NOT the run failing: it is still going on the daemon, and the next tick
|
|
21
|
+
// usually reconnects, so following continues; the caller decides what to show meanwhile.
|
|
22
|
+
readonly onError?: ((cause: unknown) => void) | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sleep = (ms: number, signal: AbortSignal | undefined): Promise<void> =>
|
|
26
|
+
new Promise((resolve) => {
|
|
27
|
+
const timer = setTimeout(done, ms);
|
|
28
|
+
function done(): void {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
signal?.removeEventListener("abort", done);
|
|
31
|
+
resolve();
|
|
32
|
+
}
|
|
33
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/* FOLLOW A RUN TO ITS VERDICT: read its state until it is no longer running, and answer with the settled run.
|
|
37
|
+
* The one loop under every surface that starts a command and waits for the answer, the web's run watcher and
|
|
38
|
+
* an extension's push pill alike, because a daemon that answers "started" at once (prepush.contract.ts,
|
|
39
|
+
* git.contract.ts push) leaves every caller to write this loop, and each one that did wrote the dropped-poll
|
|
40
|
+
* case differently. Reads once immediately, so a run that has already settled is answered without a wait. */
|
|
41
|
+
export const followCommandRun = async <R extends CommandRun>(read: () => Promise<R>, options: FollowRunOptions<R>): Promise<R | undefined> => {
|
|
42
|
+
const { intervalMs, signal, onState, onError } = options;
|
|
43
|
+
for (;;) {
|
|
44
|
+
if (signal?.aborted === true) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const run = await read();
|
|
49
|
+
onState?.(run);
|
|
50
|
+
if (run.status !== "running") {
|
|
51
|
+
return run;
|
|
52
|
+
}
|
|
53
|
+
} catch (cause) {
|
|
54
|
+
onError?.(cause);
|
|
55
|
+
}
|
|
56
|
+
await sleep(intervalMs, signal);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const commandRunOutcome = (run: Pick<CommandRun, "status" | "timedOut">, subject: string): string => {
|
|
61
|
+
if (run.timedOut === true) {
|
|
62
|
+
return `${subject} timed out`;
|
|
63
|
+
}
|
|
64
|
+
switch (run.status) {
|
|
65
|
+
case "error":
|
|
66
|
+
return `${subject} couldn't run`;
|
|
67
|
+
case "cancelled":
|
|
68
|
+
return `${subject} stopped`;
|
|
69
|
+
case "failed":
|
|
70
|
+
return `${subject} failed`;
|
|
71
|
+
case "passed":
|
|
72
|
+
return `${subject} passed`;
|
|
73
|
+
default:
|
|
74
|
+
// `idle` reaches a card only when the command was cleared between the click and the request, there
|
|
75
|
+
// is no run any more, so it says so rather than implying one ran and said nothing.
|
|
76
|
+
return `${subject} didn't run`;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -5,7 +5,7 @@ import { currentLock } from "./contract-lock.js";
|
|
|
5
5
|
/* The committed lock and the code must say the same thing: see contract-lock.ts for what the pair buys.
|
|
6
6
|
*
|
|
7
7
|
* This is the HALF that runs everywhere the tests run; the other half (a shrunk lock needs a declared break)
|
|
8
|
-
* lives in
|
|
8
|
+
* lives in _tools/checks/contract-shrink.mjs, which has git and this suite does not.
|
|
9
9
|
*
|
|
10
10
|
* ITS OWN BUDGET, because the default one is a HANG DETECTOR and this test does real work: it serializes every
|
|
11
11
|
* schema this package exports, ~500 of them, to JSON Schema. That is ~100ms with the machine to itself and it
|
package/src/contract-lock.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Two readers depend on that answer. contract-lock.test.ts fails whenever the committed file is out of step
|
|
6
6
|
* with the code, so a contract change always lands WITH its lock diff, visible in review, attributable to a
|
|
7
|
-
* commit. And
|
|
7
|
+
* commit. And the contract-shrink check (_tools/checks/) compares the committed lock against its merge-base to insist that a
|
|
8
8
|
* SHRUNK surface, a schema or field removed, a type changed, arrives as a `!` commit carrying a
|
|
9
9
|
* `Breaking-Note:` trailer, which is what feeds the release's "Breaking changes" section and the update card's
|
|
10
10
|
* warning. Additions pass freely; every persisted-manifest reader parses loosely, so growth breaks nobody.
|
|
@@ -58,7 +58,7 @@ export const currentLock = (): Record<string, unknown> => {
|
|
|
58
58
|
/* ONE LINE PER SCHEMA, on purpose, not JSON.stringify(lock, null, 4). Pretty-printed, the lock is a 35k-line
|
|
59
59
|
* wall nobody scrolls; a line per export keeps it ~500 lines and makes `git diff` read as the list of WHICH
|
|
60
60
|
* surfaces moved, which is the level a reviewer reviews at. The path-level detail lives in the tooling: the
|
|
61
|
-
* lock test diffs parsed objects and
|
|
61
|
+
* lock test diffs parsed objects and the contract-shrink check names the exact removed paths, so nothing is lost by
|
|
62
62
|
* not laying the structure out vertically. */
|
|
63
63
|
export const serializeLock = (lock: Record<string, unknown>): string =>
|
|
64
64
|
`{\n${Object.entries(lock)
|