@intentic/sandbox-contract 1.303.0 → 1.305.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 +5 -3
- package/dist/contracts/agent.contract.d.ts +195 -41
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +8 -8
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +217 -3
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +30 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +18 -5
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +3 -3
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/{host.contract.d.ts → device.contract.d.ts} +75 -2
- package/dist/contracts/device.contract.d.ts.map +1 -0
- package/dist/contracts/device.contract.js +17 -0
- package/dist/contracts/device.contract.js.map +1 -0
- package/dist/contracts/extensions.contract.d.ts +2 -2
- package/dist/contracts/git.contract.d.ts +24 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/panels.contract.d.ts +1 -1
- package/dist/contracts/personas.contract.d.ts +0 -9
- package/dist/contracts/personas.contract.d.ts.map +1 -1
- package/dist/contracts/personas.contract.js +1 -10
- package/dist/contracts/personas.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +138 -98
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.js +1 -0
- package/dist/contracts/runner.contract.js.map +1 -1
- package/dist/contracts/secrets.contract.js +2 -2
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +23 -3
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +0 -23
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +109 -8
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +11 -2
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +85 -10
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +26 -10
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts +20 -0
- package/dist/events/agent-words.d.ts.map +1 -0
- package/dist/events/agent-words.js +82 -0
- package/dist/events/agent-words.js.map +1 -0
- package/dist/events/{cards.d.ts → requests.d.ts} +18 -18
- package/dist/events/requests.d.ts.map +1 -0
- package/dist/events/{cards.js → requests.js} +26 -26
- package/dist/events/requests.js.map +1 -0
- package/dist/events/resume.d.ts +1 -0
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +2 -0
- package/dist/events/resume.js.map +1 -1
- package/dist/events/system-events.d.ts +21 -3
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +165 -25
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +34 -15
- package/dist/events/transcript.js.map +1 -1
- package/dist/events/watch-wake.d.ts.map +1 -1
- package/dist/events/watch-wake.js +4 -0
- package/dist/events/watch-wake.js.map +1 -1
- package/dist/index.d.ts +629 -129
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/models/model-offer.d.ts +2 -1
- package/dist/models/model-offer.d.ts.map +1 -1
- package/dist/models/model-offer.js +5 -4
- package/dist/models/model-offer.js.map +1 -1
- package/dist/models/model-roles.d.ts +2 -9
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +2 -9
- package/dist/models/model-roles.js.map +1 -1
- package/dist/policy/command-classes.d.ts +1 -0
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +8 -0
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/persona-home.d.ts +10 -0
- package/dist/policy/persona-home.d.ts.map +1 -0
- package/dist/policy/persona-home.js +11 -0
- package/dist/policy/persona-home.js.map +1 -0
- package/dist/policy/request-status.d.ts +5 -0
- package/dist/policy/request-status.d.ts.map +1 -0
- package/dist/policy/{card-status.js → request-status.js} +3 -3
- package/dist/policy/request-status.js.map +1 -0
- package/dist/policy/safety-policy.d.ts +1 -1
- package/dist/policy/safety-policy.d.ts.map +1 -1
- package/dist/policy/safety-policy.js +1 -0
- package/dist/policy/safety-policy.js.map +1 -1
- package/dist/policy/turned-away.d.ts +2 -0
- package/dist/policy/turned-away.d.ts.map +1 -0
- package/dist/policy/turned-away.js +13 -0
- package/dist/policy/turned-away.js.map +1 -0
- package/dist/protocol/peer-dial.d.ts +9 -0
- package/dist/protocol/peer-dial.d.ts.map +1 -1
- package/dist/protocol/peer-dial.js +12 -4
- package/dist/protocol/peer-dial.js.map +1 -1
- package/dist/protocol/peer-mcp-server.d.ts +1 -0
- package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
- package/dist/protocol/peer-mcp-server.js +14 -1
- package/dist/protocol/peer-mcp-server.js.map +1 -1
- package/dist/schemas/agent.d.ts +2 -1
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +7 -0
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +29 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +18 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +9 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/capabilities.d.ts +25 -12
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +11 -6
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/chat-route.d.ts +49 -0
- package/dist/schemas/chat-route.d.ts.map +1 -0
- package/dist/schemas/{model-route.js → chat-route.js} +17 -5
- package/dist/schemas/chat-route.js.map +1 -0
- package/dist/schemas/context-trim.d.ts +9 -0
- package/dist/schemas/context-trim.d.ts.map +1 -0
- package/dist/schemas/context-trim.js +18 -0
- package/dist/schemas/context-trim.js.map +1 -0
- package/dist/schemas/devices.d.ts +21 -8
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +8 -9
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/extension-updates.d.ts +4 -4
- package/dist/schemas/extension-updates.js +1 -1
- package/dist/schemas/extension-updates.js.map +1 -1
- package/dist/schemas/git/git.d.ts +83 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +40 -0
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +36 -5
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +7 -5
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/metrics.d.ts +108 -0
- package/dist/schemas/metrics.d.ts.map +1 -0
- package/dist/schemas/metrics.js +77 -0
- package/dist/schemas/metrics.js.map +1 -0
- package/dist/schemas/panels.d.ts +2 -2
- package/dist/schemas/panels.js +1 -1
- package/dist/schemas/panels.js.map +1 -1
- package/dist/schemas/personas.d.ts +1 -13
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +3 -20
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +1 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +6 -0
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/secrets.d.ts +1 -1
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +1 -1
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/settings.d.ts +0 -42
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +1 -16
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/shared.d.ts +2 -2
- package/dist/schemas/shared.d.ts.map +1 -1
- package/dist/schemas/shared.js +3 -3
- package/dist/schemas/shared.js.map +1 -1
- package/dist/schemas/system-prompt.d.ts +104 -0
- package/dist/schemas/system-prompt.d.ts.map +1 -0
- package/dist/schemas/system-prompt.js +25 -0
- package/dist/schemas/system-prompt.js.map +1 -0
- package/dist/schemas/workflows.d.ts +2 -2
- package/dist/schemas/workflows.d.ts.map +1 -1
- package/dist/schemas/workflows.js +2 -2
- package/dist/schemas/workflows.js.map +1 -1
- package/dist/state/definition.d.ts +18 -10
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/fix-attempt-plan.d.ts +4 -0
- package/dist/state/fix-attempt-plan.d.ts.map +1 -1
- package/dist/state/fix-attempt-plan.js +3 -2
- package/dist/state/fix-attempt-plan.js.map +1 -1
- package/dist/state/fix-stance.d.ts +1 -0
- package/dist/state/fix-stance.d.ts.map +1 -1
- package/dist/state/fix-stance.js +11 -0
- package/dist/state/fix-stance.js.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +8 -1
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +0 -1
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +0 -1
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +4 -4
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +4 -4
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/documents.d.ts +2 -2
- package/dist/text/documents.d.ts.map +1 -1
- package/dist/text/documents.js.map +1 -1
- package/dist/text/mentions.d.ts +1 -0
- package/dist/text/mentions.d.ts.map +1 -1
- package/dist/text/mentions.js +5 -1
- package/dist/text/mentions.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +8 -3
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +83 -28
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +9 -9
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/probes.test.ts +5 -2
- package/src/chores/stack.test.ts +1 -1
- package/src/chores/verdict.test.ts +6 -5
- package/src/contracts/agent.contract.ts +10 -9
- package/src/contracts/agents.contract.ts +35 -2
- package/src/contracts/capabilities.contract.ts +3 -3
- package/src/contracts/{host.contract.ts → device.contract.ts} +8 -6
- package/src/contracts/personas.contract.ts +1 -13
- package/src/contracts/runner.contract.ts +3 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/system.contract.ts +13 -2
- package/src/contracts/webext.contract.ts +1 -1
- package/src/events/agent-events.ts +37 -14
- package/src/events/agent-words.test.ts +68 -0
- package/src/events/agent-words.ts +124 -0
- package/src/events/{cards.ts → requests.ts} +44 -44
- package/src/events/resume.test.ts +8 -1
- package/src/events/resume.ts +4 -0
- package/src/events/transcript.ts +51 -22
- package/src/events/watch-wake.test.ts +3 -2
- package/src/events/watch-wake.ts +4 -0
- package/src/ids/conversation-ids.test.ts +3 -3
- package/src/ids/hostnames.test.ts +1 -1
- package/src/ids/share-paths.test.ts +1 -1
- package/src/ids/tunnel-ids.test.ts +1 -1
- package/src/index.ts +10 -4
- package/src/models/agent-catalog.test.ts +1 -1
- package/src/models/capability-ledger.test.ts +1 -1
- package/src/models/model-offer.test.ts +9 -2
- package/src/models/model-offer.ts +10 -7
- package/src/models/model-order.test.ts +1 -1
- package/src/models/model-pins.test.ts +1 -1
- package/src/models/model-roles.test.ts +1 -1
- package/src/models/model-roles.ts +6 -12
- package/src/models/plan-pools.test.ts +1 -1
- package/src/models/provider-specs.test.ts +3 -3
- package/src/policy/batch-runs.test.ts +1 -1
- package/src/policy/command-classes.test.ts +37 -2
- package/src/policy/command-classes.ts +16 -0
- package/src/policy/credential-material.test.ts +6 -10
- package/src/policy/fence-paths.test.ts +1 -1
- package/src/policy/overlay-lint.test.ts +1 -1
- package/src/policy/owner-ticket.test.ts +1 -1
- package/src/policy/persona-home.test.ts +52 -0
- package/src/policy/persona-home.ts +39 -0
- package/src/policy/{card-status.ts → request-status.ts} +6 -6
- package/src/policy/safety-policy.test.ts +1 -1
- package/src/policy/safety-policy.ts +1 -0
- package/src/policy/search-globs.test.ts +1 -1
- package/src/policy/turned-away.ts +18 -0
- package/src/protocol/container-requirements.test.ts +1 -1
- package/src/protocol/ingress-contract.test.ts +8 -2
- package/src/protocol/ingress-protocol.test.ts +5 -4
- package/src/protocol/peer-dial.test.ts +111 -41
- package/src/protocol/peer-dial.ts +39 -4
- package/src/protocol/peer-mcp-server.test.ts +37 -7
- package/src/protocol/peer-mcp-server.ts +22 -1
- package/src/protocol/routes.test.ts +4 -2
- package/src/schemas/agent.ts +13 -0
- package/src/schemas/agents.ts +33 -0
- package/src/schemas/areas.test.ts +2 -8
- package/src/schemas/automations.ts +5 -5
- package/src/schemas/capabilities.ts +20 -9
- package/src/schemas/chat-route.ts +61 -0
- package/src/schemas/context-trim.test.ts +38 -0
- package/src/schemas/context-trim.ts +29 -0
- package/src/schemas/dev-rebuild-log.test.ts +1 -1
- package/src/schemas/devices.ts +25 -22
- package/src/schemas/extension-updates.ts +1 -1
- package/src/schemas/git/git.test.ts +11 -0
- package/src/schemas/git/git.ts +66 -0
- package/src/schemas/hosts.ts +15 -8
- package/src/schemas/issues.ts +1 -1
- package/src/schemas/metrics.ts +107 -0
- package/src/schemas/panels.ts +2 -2
- package/src/schemas/personas.ts +5 -28
- package/src/schemas/providers/plan-limits.ts +9 -0
- package/src/schemas/secrets.ts +1 -1
- package/src/schemas/settings.test.ts +1 -1
- package/src/schemas/settings.ts +9 -36
- package/src/schemas/shared.ts +6 -6
- package/src/schemas/system-prompt.ts +41 -0
- package/src/schemas/version-seam.test.ts +1 -1
- package/src/schemas/webext.ts +1 -1
- package/src/schemas/workflows.ts +4 -4
- package/src/state/contract-lock.test.ts +15 -11
- package/src/state/fix-attempt-plan.test.ts +10 -1
- package/src/state/fix-attempt-plan.ts +6 -2
- package/src/state/fix-stance.test.ts +10 -1
- package/src/state/fix-stance.ts +13 -0
- package/src/state/history-state.ts +17 -1
- package/src/state/runtime-state.test.ts +3 -9
- package/src/state/runtime-state.ts +0 -4
- package/src/state/versions.test.ts +1 -1
- package/src/state/workspace-state.test.ts +12 -7
- package/src/state/workspace-state.ts +4 -4
- package/src/text/archives.test.ts +1 -1
- package/src/text/documents.test.ts +1 -1
- package/src/text/documents.ts +2 -2
- package/src/text/embed.test.ts +17 -6
- package/src/text/emoji.test.ts +1 -1
- package/src/text/mentions.test.ts +20 -2
- package/src/text/mentions.ts +10 -1
- package/src/text/path-refs.test.ts +1 -1
- package/src/text/title.test.ts +1 -1
- package/src/text/transcript-fold.test.ts +183 -1
- package/src/text/transcript-fold.ts +146 -39
- package/src/text/whisper.test.ts +1 -1
- package/src/text/workflow-faults.test.ts +1 -1
- package/src/time/zone.test.ts +6 -6
- package/dist/contracts/host.contract.d.ts.map +0 -1
- package/dist/contracts/host.contract.js +0 -16
- package/dist/contracts/host.contract.js.map +0 -1
- package/dist/events/cards.d.ts.map +0 -1
- package/dist/events/cards.js.map +0 -1
- package/dist/policy/card-status.d.ts +0 -5
- package/dist/policy/card-status.d.ts.map +0 -1
- package/dist/policy/card-status.js.map +0 -1
- package/dist/schemas/model-route.d.ts +0 -27
- package/dist/schemas/model-route.d.ts.map +0 -1
- package/dist/schemas/model-route.js.map +0 -1
- package/src/schemas/model-route.ts +0 -43
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// The one outbound socket a peer (machine agent, webext, runner) holds to its sandbox: hello carries the enrollment
|
|
2
2
|
// token in the frame, then the link is pure oRPC. Reconnects on backoff after any drop except a revoked enrollment
|
|
3
3
|
// (code 1008), which never retries.
|
|
4
|
+
//
|
|
5
|
+
// THE RULE BOTH ENDS ARE HELD TO: 1008 means the sandbox READ its enrollment store and does not hold this token. It
|
|
6
|
+
// costs the far end its pairing — someone has to walk to that machine and paste a command — so it is the one refusal
|
|
7
|
+
// that may never stand in for a moment the sandbox was having. A manifest mid-write, a capability card not yet
|
|
8
|
+
// restored after a recreate, a hello that arrived late: all of those are PEER_TRY_AGAIN, and all of them used to be
|
|
9
|
+
// 1008.
|
|
4
10
|
|
|
5
11
|
// Reconnect backoff: fast floor for a restart, low cap so a reopened laptop is back within a minute.
|
|
6
12
|
export const PEER_LINK_BACKOFF = { floorMs: 1_000, capMs: 30_000, stableMs: 60_000 } as const;
|
|
@@ -9,12 +15,24 @@ export const PEER_LINK_BACKOFF = { floorMs: 1_000, capMs: 30_000, stableMs: 60_0
|
|
|
9
15
|
const LOUD_ATTEMPTS = 3;
|
|
10
16
|
const QUIET_LOG_MS = 10 * 60_000;
|
|
11
17
|
|
|
18
|
+
/* HOW OFTEN A LINK NOBODY IS ANSWERING MAY TRY, once "the sandbox is restarting" has stopped being a plausible
|
|
19
|
+
reading of the silence. The ladder above caps at 30s and stays there forever, which is right for the first minutes
|
|
20
|
+
and absurd after the first week: a machine holding a link to a sandbox that no longer exists spent 2,880 attempts a
|
|
21
|
+
day on it, and most of a 7 MB log saying so. The link is never given up — a laptop closed for a fortnight must find
|
|
22
|
+
its sandboxes again — it just stops asking every half minute. Reaching the far end once resets it (`failures = 0`),
|
|
23
|
+
so an outage that ends is back on the fast ladder immediately. */
|
|
24
|
+
export const LONG_OUTAGE_ATTEMPTS = 20;
|
|
25
|
+
export const LONG_OUTAGE_MS = 15 * 60_000;
|
|
26
|
+
|
|
12
27
|
/* HOW LONG A SOCKET MAY SAY NOTHING before this side calls the link dead, as a multiple of the door's own heartbeat. */
|
|
13
28
|
export const PEER_LINK_SILENCE_HEARTBEATS = 3;
|
|
14
29
|
export const peerLinkSilenceMs = (heartbeatMs: number): number => heartbeatMs * PEER_LINK_SILENCE_HEARTBEATS;
|
|
15
30
|
|
|
16
31
|
// The sandbox closes with this when the token is not enrolled: a decision, not a fault, and one that never heals.
|
|
17
|
-
const
|
|
32
|
+
export const PEER_UNAUTHORIZED = 1008;
|
|
33
|
+
// Refused, but not about this peer's credential: the sandbox could not decide right now. Retried on the ordinary
|
|
34
|
+
// ladder, which is the whole difference between the two.
|
|
35
|
+
export const PEER_TRY_AGAIN = 1013;
|
|
18
36
|
|
|
19
37
|
// WebSocket spec's readyState value for open, named so a fake socket need not import the real class.
|
|
20
38
|
const OPEN = 1;
|
|
@@ -50,11 +68,20 @@ export interface PeerDialSpec<S extends SocketLike> {
|
|
|
50
68
|
// it for `status`, which otherwise has only the link list on disk and no idea whether any of it is up).
|
|
51
69
|
export type PeerLinkState = "open" | "connecting" | "closed";
|
|
52
70
|
|
|
71
|
+
// How long this link has been failing and how many attempts it has spent, for the readers that are not this process.
|
|
72
|
+
// Absent while the socket is open, so "is anything wrong here" is answerable without publishing a healthy link's
|
|
73
|
+
// history. `since` is the first failure of THIS outage: one answer resets it.
|
|
74
|
+
export interface PeerOutage {
|
|
75
|
+
readonly failures: number;
|
|
76
|
+
readonly since: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
export interface PeerLink {
|
|
54
80
|
// Resolves when the loop is asked to stop or refused for good; never rejects, a connection error is a retry.
|
|
55
81
|
readonly done: Promise<void>;
|
|
56
82
|
readonly stop: (reason?: string) => void;
|
|
57
83
|
readonly state: () => PeerLinkState;
|
|
84
|
+
readonly outage: () => PeerOutage | undefined;
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink => {
|
|
@@ -73,6 +100,8 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
73
100
|
// loud: between them they are the whole of the quiet rule above.
|
|
74
101
|
let failures = 0;
|
|
75
102
|
let quietSince = 0;
|
|
103
|
+
// When the CURRENT outage began, as opposed to when this link was last complained about.
|
|
104
|
+
let failingSince: number | undefined;
|
|
76
105
|
|
|
77
106
|
/* What ONE failed attempt is allowed to say. */
|
|
78
107
|
const complain = (said: string, delay: number): void => {
|
|
@@ -135,9 +164,11 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
135
164
|
resolveDone();
|
|
136
165
|
return;
|
|
137
166
|
}
|
|
138
|
-
const
|
|
167
|
+
const rung = spec.backoff.next(openedAt === undefined ? 0 : Date.now() - openedAt);
|
|
139
168
|
openedAt = undefined;
|
|
140
169
|
failures += 1;
|
|
170
|
+
failingSince ??= Date.now();
|
|
171
|
+
const delay = failures >= LONG_OUTAGE_ATTEMPTS ? Math.max(rung, LONG_OUTAGE_MS) : rung;
|
|
141
172
|
complain(said, delay);
|
|
142
173
|
waiting = true;
|
|
143
174
|
setTimeout(() => void open(), delay);
|
|
@@ -166,6 +197,7 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
166
197
|
// "connected to …" line this open is about to log is what reports the recovery.
|
|
167
198
|
failures = 0;
|
|
168
199
|
quietSince = 0;
|
|
200
|
+
failingSince = undefined;
|
|
169
201
|
arm();
|
|
170
202
|
spec.attach(ws);
|
|
171
203
|
const send = (hello: Record<string, unknown>): void => {
|
|
@@ -185,7 +217,7 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
185
217
|
|
|
186
218
|
ws.addEventListener("close", (event) => {
|
|
187
219
|
/* A revocation is answered here rather than through `drop`: it is the one close that ends the loop. */
|
|
188
|
-
if (event.code ===
|
|
220
|
+
if (event.code === PEER_UNAUTHORIZED) {
|
|
189
221
|
dropped = true;
|
|
190
222
|
disarm();
|
|
191
223
|
if (socket === ws) {
|
|
@@ -196,7 +228,9 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
196
228
|
spec.revoked();
|
|
197
229
|
return;
|
|
198
230
|
}
|
|
199
|
-
|
|
231
|
+
// Named rather than numbered, because this is the code a reader most needs not to mistake for the one
|
|
232
|
+
// above it: the sandbox is up and talking, it just would not admit this socket this time.
|
|
233
|
+
drop(event.code === PEER_TRY_AGAIN ? "the sandbox is not ready to admit this connection yet" : `disconnected (${event.code ?? "no code"})`);
|
|
200
234
|
});
|
|
201
235
|
|
|
202
236
|
/* Socket errors record a cause; the close event owns retry scheduling. */
|
|
@@ -222,5 +256,6 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
222
256
|
resolveDone();
|
|
223
257
|
},
|
|
224
258
|
state: () => (socket?.readyState === OPEN ? "open" : socket !== undefined || waiting ? "connecting" : "closed"),
|
|
259
|
+
outage: () => (failingSince === undefined ? undefined : { failures, since: failingSince }),
|
|
225
260
|
};
|
|
226
261
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createMcpServer, type McpAuditEntry, textResult, tool } from "./peer-mcp-server.js";
|
|
4
4
|
|
|
@@ -37,7 +37,8 @@ const build = () => {
|
|
|
37
37
|
return { handle, audits };
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const call = (name: string, args: Record<string, unknown>, allowed = true) =>
|
|
40
|
+
const call = (name: string, args: Record<string, unknown>, allowed = true) =>
|
|
41
|
+
build().handle({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, { allowed });
|
|
41
42
|
|
|
42
43
|
test("initialize names the peer and its build; ping answers empty; an unknown method is a JSON-RPC error", async () => {
|
|
43
44
|
const { handle } = build();
|
|
@@ -52,14 +53,38 @@ test("initialize names the peer and its build; ping answers empty; an unknown me
|
|
|
52
53
|
|
|
53
54
|
test("tools/list publishes each tool's schema as JSON Schema, without the dialect line", async () => {
|
|
54
55
|
const { handle } = build();
|
|
55
|
-
const listed = (await handle({ jsonrpc: "2.0", id: 1, method: "tools/list" }, { allowed: true })) as {
|
|
56
|
+
const listed = (await handle({ jsonrpc: "2.0", id: 1, method: "tools/list" }, { allowed: true })) as {
|
|
57
|
+
result: { tools: { name: string; inputSchema: Record<string, unknown> }[] };
|
|
58
|
+
};
|
|
56
59
|
expect(listed.result.tools.map((entry) => entry.name)).toEqual(["echo", "fail"]);
|
|
57
60
|
expect(listed.result.tools[0]?.inputSchema).toMatchObject({ type: "object", properties: { text: { type: "string" } } });
|
|
58
61
|
expect(listed.result.tools[0]?.inputSchema).not.toHaveProperty("$schema");
|
|
59
62
|
});
|
|
60
63
|
|
|
64
|
+
test("a tuple is published without the boolean `items` llama.cpp's grammar converter refuses, and still checks its length", async () => {
|
|
65
|
+
const listed = tool({
|
|
66
|
+
name: "point",
|
|
67
|
+
description: "A point.",
|
|
68
|
+
input: z.object({ at: z.tuple([z.number(), z.number()]), path: z.array(z.tuple([z.number(), z.number()])) }),
|
|
69
|
+
run: async () => textResult("ok"),
|
|
70
|
+
});
|
|
71
|
+
expect(JSON.stringify(listed.inputSchema)).not.toContain("false");
|
|
72
|
+
expect(listed.inputSchema).toMatchObject({
|
|
73
|
+
properties: {
|
|
74
|
+
at: { type: "array", prefixItems: [{ type: "number" }, { type: "number" }], minItems: 2, maxItems: 2 },
|
|
75
|
+
path: { items: { type: "array", prefixItems: [{ type: "number" }, { type: "number" }], maxItems: 2 } },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const parsed = (await listed.call({ at: [1, 2, 3], path: [] }, undefined)) as { isError: boolean };
|
|
79
|
+
expect(parsed.isError).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
61
82
|
test("a call is checked against the same schema, and a bad argument is a readable result, not a fault", async () => {
|
|
62
|
-
expect(await call("echo", { text: "hi" })).toEqual({
|
|
83
|
+
expect(await call("echo", { text: "hi" })).toEqual({
|
|
84
|
+
jsonrpc: "2.0",
|
|
85
|
+
id: 1,
|
|
86
|
+
result: { content: [{ type: "text", text: "hi" }], isError: false },
|
|
87
|
+
});
|
|
63
88
|
const bad = (await call("echo", { text: "" })) as { result: { content: { text: string }[]; isError: boolean } };
|
|
64
89
|
expect(bad.result.isError).toBe(true);
|
|
65
90
|
expect(bad.result.content[0]?.text).toContain("text");
|
|
@@ -75,8 +100,11 @@ test("a tool this peer does not have answers in the peer's own words", async ()
|
|
|
75
100
|
|
|
76
101
|
test("a thrown refusal and a thrown failure are both results, and the audit line says which", async () => {
|
|
77
102
|
const { handle, audits } = build();
|
|
78
|
-
const refused = (await handle(
|
|
79
|
-
|
|
103
|
+
const refused = (await handle(
|
|
104
|
+
{ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "echo", arguments: { text: "hi" } } },
|
|
105
|
+
{ allowed: false },
|
|
106
|
+
)) as {
|
|
107
|
+
result: { content: { type: string; text: string }[]; isError: boolean };
|
|
80
108
|
};
|
|
81
109
|
expect(refused.result).toEqual({ content: [{ type: "text", text: "the switch is off" }], isError: true });
|
|
82
110
|
await handle({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "fail", arguments: {} } }, { allowed: true });
|
|
@@ -99,5 +127,7 @@ test("an audit log that cannot be written never fails the answer", async () => {
|
|
|
99
127
|
throw new Error("disk full");
|
|
100
128
|
},
|
|
101
129
|
});
|
|
102
|
-
expect(await handle({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "hi", arguments: {} } }, undefined)).toMatchObject({
|
|
130
|
+
expect(await handle({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "hi", arguments: {} } }, undefined)).toMatchObject({
|
|
131
|
+
result: { isError: false },
|
|
132
|
+
});
|
|
103
133
|
});
|
|
@@ -22,6 +22,27 @@ export interface McpTool<Ctx> {
|
|
|
22
22
|
|
|
23
23
|
export const textResult = (text: string, isError = false): Record<string, unknown> => ({ content: [{ type: "text", text }], isError });
|
|
24
24
|
|
|
25
|
+
// Every tool schema has to survive llama.cpp's grammar converter, which is how a local model is held to a tool's shape:
|
|
26
|
+
// it reads `items` before `prefixItems` and refuses a boolean schema, so zod's closed-tuple form (`items: false`) fails
|
|
27
|
+
// the conversion of the WHOLE tool set, not just its own tool. Length is what closes a tuple, and `maxItems` still says
|
|
28
|
+
// it, so no other reader sees a different shape. Applied again by the daemon's bridge, whose peers are separately
|
|
29
|
+
// installed software that can be any age.
|
|
30
|
+
export const converterReadable = (value: unknown): unknown => {
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.map(converterReadable);
|
|
33
|
+
}
|
|
34
|
+
if (typeof value !== "object" || value === null) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
const node = value as Record<string, unknown>;
|
|
38
|
+
const prefix = node["prefixItems"];
|
|
39
|
+
const tupleLength = node["items"] === false && Array.isArray(prefix) ? prefix.length : undefined;
|
|
40
|
+
const walked = Object.fromEntries(
|
|
41
|
+
Object.entries(node).flatMap(([key, child]) => (tupleLength !== undefined && key === "items" ? [] : [[key, converterReadable(child)] as const])),
|
|
42
|
+
);
|
|
43
|
+
return tupleLength === undefined ? walked : { ...walked, maxItems: tupleLength };
|
|
44
|
+
};
|
|
45
|
+
|
|
25
46
|
// Builds one tool from a single zod schema; the generic carries its type to the handler, and `McpTool` erases it again
|
|
26
47
|
// since the dispatch table holds every tool. `$schema` is dropped from the JSON Schema output as redundant.
|
|
27
48
|
export const tool = <Schema extends z.ZodType, Ctx>(spec: {
|
|
@@ -34,7 +55,7 @@ export const tool = <Schema extends z.ZodType, Ctx>(spec: {
|
|
|
34
55
|
return {
|
|
35
56
|
name: spec.name,
|
|
36
57
|
description: spec.description,
|
|
37
|
-
inputSchema,
|
|
58
|
+
inputSchema: converterReadable(inputSchema) as Record<string, unknown>,
|
|
38
59
|
call: async (args, ctx) => {
|
|
39
60
|
const parsed = spec.input.safeParse(args);
|
|
40
61
|
// Readable enough for a model to fix its own call: which field, and what was expected.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { eventIterator, oc } from "@orpc/contract";
|
|
2
|
-
import { describe,
|
|
2
|
+
import { describe, it, expect } from "bun:test";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { SANDBOX_ROUTE_NAMES, SANDBOX_ROUTE_SHAPES, SANDBOX_ROUTES, sandboxRouteName } from "../index.js";
|
|
5
5
|
import { contractRoutes, routeNameForRequest, routeShapes, streamOf } from "./routes.js";
|
|
@@ -183,7 +183,9 @@ describe(`the real sandbox contract`, () => {
|
|
|
183
183
|
`system.manageDeviceSandbox`,
|
|
184
184
|
`system.runDeviceAgentFlow`,
|
|
185
185
|
`vpn.connect`,
|
|
186
|
-
]
|
|
186
|
+
]
|
|
187
|
+
.filter((name) => name in SANDBOX_ROUTE_SHAPES)
|
|
188
|
+
.toSorted(),
|
|
187
189
|
).toEqual([
|
|
188
190
|
`agent.attach`,
|
|
189
191
|
`capabilities.add`,
|
package/src/schemas/agent.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { CONVERSATION_ID } from "../ids/conversation-ids.js";
|
|
|
3
3
|
import { ModelRoleSchema } from "../models/model-roles.js";
|
|
4
4
|
import { NATIVE_PROVIDERS } from "../models/provider-specs.js";
|
|
5
5
|
import { AgentPlacementSchema } from "../protocol/runner-protocol.js";
|
|
6
|
+
import { MENTION_LIMIT } from "../text/mentions.js";
|
|
6
7
|
import { entryId } from "./internal.js";
|
|
7
8
|
// Agent runtimes the daemon can serve: native providers have dedicated adapters, `endpoint/<id>` names an installed
|
|
8
9
|
// endpoint capability, anything else is an ACP agent capability id. A bare string, not an enum, so an unknown id is a
|
|
@@ -77,6 +78,9 @@ export type CommandLocus = z.infer<typeof CommandLocusSchema>;
|
|
|
77
78
|
export const CommandClassSchema = z.enum([
|
|
78
79
|
// Rewrites or discards committed work: force-push, hard reset, force-delete a branch, clean -f, filter-branch.
|
|
79
80
|
"git.destructive",
|
|
81
|
+
// Moves a checkout to another branch. Only ever matches in a tree the conversation SHARES with its owner: inside
|
|
82
|
+
// its own copy a conversation may stand where it likes, and the drift that causes is reported, not refused.
|
|
83
|
+
"git.branch-switch",
|
|
80
84
|
// Recursive-force deletion (`rm -rf`), and its spelling in a script (`fs.rm(p, { recursive: true })`).
|
|
81
85
|
"files.destructive",
|
|
82
86
|
// State nothing recovers at this locus; which roots count (/, /history in the sandbox) depends on the locus.
|
|
@@ -136,6 +140,15 @@ export const AgentTurnSchema = z
|
|
|
136
140
|
.max(20)
|
|
137
141
|
.optional()
|
|
138
142
|
.describe("Files to hand the agent along with the prompt, as workspace paths. Upload them first."),
|
|
143
|
+
// Read out of the prompt's own `@path` tokens rather than chosen: a tokenizer over pasted text guesses, so a
|
|
144
|
+
// miss here is dropped and only a chosen attachment can refuse the turn.
|
|
145
|
+
mentions: z
|
|
146
|
+
.array(z.string().min(1))
|
|
147
|
+
.max(MENTION_LIMIT)
|
|
148
|
+
.optional()
|
|
149
|
+
.describe(
|
|
150
|
+
"Workspace paths the prompt mentions with `@`. Unlike attachments, one that escapes the workspace or names no file is ignored rather than refused.",
|
|
151
|
+
),
|
|
139
152
|
// Which provider serves the turn, absent = claude; a sessionId resumes only on the provider that minted it.
|
|
140
153
|
agent: AgentProviderSchema.optional().describe("Which model provider serves this turn. Leave it out for Claude."),
|
|
141
154
|
// Which agentic loop runs the turn, absent = provider's own; "claude-code" forces the SDK loop for any
|
package/src/schemas/agents.ts
CHANGED
|
@@ -291,6 +291,14 @@ export const AgentSummarySchema = z.object({
|
|
|
291
291
|
.describe(
|
|
292
292
|
"Who asked for the first turn, as the sandbox verified it: a member's email, token:<label> for a program's control token, or agent:<conversation id> for a child another conversation spawned. Absent when nothing was verified (a wake, a loopback caller).",
|
|
293
293
|
),
|
|
294
|
+
// The fence it was born with. Rides the summary because it is what decides who may see the row at all
|
|
295
|
+
// (auth/fleet-scope.ts visibleTo); without it here a fenced reader's board would be empty of even their own work.
|
|
296
|
+
areas: z
|
|
297
|
+
.array(z.string())
|
|
298
|
+
.optional()
|
|
299
|
+
.describe(
|
|
300
|
+
"Which named areas of the workspace this conversation was started within, latched from whoever asked for the first turn. Absent means its starter held the whole workspace, which is why a fenced member is not shown it.",
|
|
301
|
+
),
|
|
294
302
|
// Responsibility, beside provenance: the starter says who asked, this says who answers for it now. Absent means
|
|
295
303
|
// nobody has claimed it, which is what a program's or an automation's conversation is until somebody does.
|
|
296
304
|
owner: SessionOwnerSchema.optional().describe(
|
|
@@ -436,6 +444,22 @@ export const AgentSummarySchema = z.object({
|
|
|
436
444
|
.describe(
|
|
437
445
|
"Outside conditions this conversation is parked on, each of which will wake it. Absent means none, which is nearly every conversation: an armed watch is why a finished-looking agent starts working by itself, and why a hosted machine will not go idle.",
|
|
438
446
|
),
|
|
447
|
+
// In memory only: a daemon restart leaves transcript rows naming jobs this list no longer carries.
|
|
448
|
+
jobs: z
|
|
449
|
+
.array(
|
|
450
|
+
z.object({
|
|
451
|
+
id: z.string().describe("The daemon's handle for this job, the one its transcript row names."),
|
|
452
|
+
label: z.string().describe("What the job is, in the agent's own words when it gave any, else its command on one line."),
|
|
453
|
+
session: z.string().describe("The terminal session its pane runs in, which is what opening it focuses."),
|
|
454
|
+
startedAt: z.number().describe("When it started, in milliseconds."),
|
|
455
|
+
endedAt: z.number().optional().describe("When the command exited, in milliseconds. Absent while it runs."),
|
|
456
|
+
exitCode: z.number().int().optional().describe("The code it exited with. Absent while it runs, or when its exit left none."),
|
|
457
|
+
}),
|
|
458
|
+
)
|
|
459
|
+
.optional()
|
|
460
|
+
.describe(
|
|
461
|
+
"Commands this conversation left running in the background, and how the most recent ones ended. Absent means none since the daemon started. A job is running exactly while it has no end.",
|
|
462
|
+
),
|
|
439
463
|
// Epoch ms it was archived; nothing is lost (branch, transcript, counters stay), and unarchiving re-attaches a
|
|
440
464
|
// fresh worktree from the branch.
|
|
441
465
|
archivedAt: z
|
|
@@ -448,6 +472,8 @@ export const AgentSummarySchema = z.object({
|
|
|
448
472
|
export type AgentSummary = z.infer<typeof AgentSummarySchema>;
|
|
449
473
|
// One armed watch as a card carries it; derived from AgentSummarySchema.watches so the two shapes can't drift apart.
|
|
450
474
|
export type AgentWatch = NonNullable<AgentSummary["watches"]>[number];
|
|
475
|
+
// One background job as a card carries it.
|
|
476
|
+
export type AgentJob = NonNullable<AgentSummary["jobs"]>[number];
|
|
451
477
|
// AgentsListSchema is declared later, after AutomationApprovalSchema, since the fleet list carries held wakes and zod
|
|
452
478
|
// needs that type declared first.
|
|
453
479
|
export const AgentIdSchema = z.object({ id: z.string().min(1).describe("Which conversation.") });
|
|
@@ -665,6 +691,13 @@ export type LandConflict = z.infer<typeof LandConflictSchema>;
|
|
|
665
691
|
// hand.
|
|
666
692
|
export const LandResultSchema = z.object({
|
|
667
693
|
landed: z.boolean().describe("Whether the entire composed change was applied."),
|
|
694
|
+
// Merged and nothing moved is the one outcome with nothing to show for itself; without this the caller cannot tell
|
|
695
|
+
// it from a merge that carried work, and a press that did nothing looked exactly like a press that worked.
|
|
696
|
+
changed: z
|
|
697
|
+
.boolean()
|
|
698
|
+
.describe(
|
|
699
|
+
"Whether anything actually moved. False alongside merged means there was nothing on the branch to apply: the work is already in your tree, or the branch never carried any.",
|
|
700
|
+
),
|
|
668
701
|
conflicts: z.array(LandConflictSchema).optional().describe("What stopped the whole composed change, grouped per repository."),
|
|
669
702
|
resolving: z
|
|
670
703
|
.array(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PUBLIC_DIR, STATE_DIR } from "@intentic/constants";
|
|
2
|
-
import { describe,
|
|
2
|
+
import { describe, test, expect } from "bun:test";
|
|
3
3
|
import { AreaFolderSchema } from "./areas.js";
|
|
4
4
|
|
|
5
5
|
// What an area may name. These refusals are load-bearing rather than cosmetic: an area's folders become the fence
|
|
@@ -22,13 +22,7 @@ describe("AreaFolderSchema", () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test("the sandbox's own configuration and its outbox can never be named", () => {
|
|
25
|
-
for (const folder of [
|
|
26
|
-
STATE_DIR,
|
|
27
|
-
`${STATE_DIR}/config`,
|
|
28
|
-
`${STATE_DIR}/config/hooks`,
|
|
29
|
-
PUBLIC_DIR,
|
|
30
|
-
`${PUBLIC_DIR}/site`,
|
|
31
|
-
]) {
|
|
25
|
+
for (const folder of [STATE_DIR, `${STATE_DIR}/config`, `${STATE_DIR}/config/hooks`, PUBLIC_DIR, `${PUBLIC_DIR}/site`]) {
|
|
32
26
|
expect(AreaFolderSchema.safeParse(folder).success, folder).toBe(false);
|
|
33
27
|
}
|
|
34
28
|
});
|
|
@@ -124,7 +124,7 @@ export const TriggerSchema = z.discriminatedUnion("kind", [
|
|
|
124
124
|
}),
|
|
125
125
|
]);
|
|
126
126
|
export type Trigger = z.infer<typeof TriggerSchema>;
|
|
127
|
-
// The
|
|
127
|
+
// The Visitor chat widget's settings, present only on `webchat` listener automations. Split into what the widget itself
|
|
128
128
|
// may read (public by construction) and what only the daemon may (turnstileSecret); GET /webchat/<id>/config serves the
|
|
129
129
|
// first group by naming it, never by omitting the second.
|
|
130
130
|
export const WebchatConfigSchema = z.object({
|
|
@@ -177,7 +177,7 @@ export const WebchatConfigSchema = z.object({
|
|
|
177
177
|
sessionTtlMinutes: z.number().int().positive().optional(),
|
|
178
178
|
});
|
|
179
179
|
export type WebchatConfig = z.infer<typeof WebchatConfigSchema>;
|
|
180
|
-
// Default daily agent-turn ceiling for an unconfigured
|
|
180
|
+
// Default daily agent-turn ceiling for an unconfigured Visitor chat; high enough for real traffic, low enough to blunt a
|
|
181
181
|
// script.
|
|
182
182
|
export const WEBCHAT_DAILY_MAX_DEFAULT = 200;
|
|
183
183
|
// The widget wire: the shapes GET /webchat/<id>/config, GET .../challenge and POST .../message speak, living beside the
|
|
@@ -201,7 +201,7 @@ export const WebchatPublicConfigSchema = z.object({
|
|
|
201
201
|
});
|
|
202
202
|
export type WebchatPublicConfig = z.infer<typeof WebchatPublicConfigSchema>;
|
|
203
203
|
// A proof-of-work challenge: find a nonce whose SHA-256 of `${salt}:${nonce}` starts with `difficulty` zero bits. One
|
|
204
|
-
// shape for every public door (
|
|
204
|
+
// shape for every public door (Visitor chat, bug intake), and one matching solver, embed.ts.
|
|
205
205
|
export const PowChallengeSchema = z.object({ salt: z.string(), difficulty: z.number().int().positive() });
|
|
206
206
|
export type PowChallenge = z.infer<typeof PowChallengeSchema>;
|
|
207
207
|
// One visitor message; `conversationId` is the widget's own localStorage id, a thread key rather than a secret — the
|
|
@@ -224,7 +224,7 @@ export const WebchatMessageSchema = z.object({
|
|
|
224
224
|
});
|
|
225
225
|
export type WebchatMessage = z.infer<typeof WebchatMessageSchema>;
|
|
226
226
|
// What GET /webchat/<id>/messages answers: replies queued after the visitor's stream closed, which is every reply an
|
|
227
|
-
// approval-gated
|
|
227
|
+
// approval-gated guest produces and every one a human writes as the agent. Public by construction, like the config:
|
|
228
228
|
// reaching it needs nothing but an allowed origin and the conversation id already in that browser.
|
|
229
229
|
export const WebchatPendingSchema = z.object({
|
|
230
230
|
// Oldest first, so appending them to the log preserves the order they were written in.
|
|
@@ -272,7 +272,7 @@ export const AutomationSchema = z.object({
|
|
|
272
272
|
"A command run before the wake that decides whether there is anything to do. Skipped by the guard is often the most useful thing an automation can report.",
|
|
273
273
|
),
|
|
274
274
|
prompt: z.string().min(1).describe("What the woken agent is told."),
|
|
275
|
-
// The
|
|
275
|
+
// The Visitor chat widget's settings, `webchat` listener automations only, ignored on every other trigger.
|
|
276
276
|
webchat: WebchatConfigSchema.optional().describe("Settings for the public chat widget, for an automation that answers visitors."),
|
|
277
277
|
// Bug intake settings, `issues` triggers only; its own field rather than a shared bag, since a chat's and an
|
|
278
278
|
// intake's fields mostly don't overlap.
|
|
@@ -24,12 +24,13 @@ export const CapabilityKindSchema = z.enum([
|
|
|
24
24
|
"docker",
|
|
25
25
|
"browser",
|
|
26
26
|
"identity",
|
|
27
|
-
"
|
|
27
|
+
"device",
|
|
28
28
|
"webext",
|
|
29
29
|
"agent",
|
|
30
30
|
"endpoint",
|
|
31
31
|
"localmodel",
|
|
32
32
|
"wallet",
|
|
33
|
+
"fleet",
|
|
33
34
|
]);
|
|
34
35
|
export type CapabilityKind = z.infer<typeof CapabilityKindSchema>;
|
|
35
36
|
export const CapabilityStateSchema = z.enum(["active", "pending", "error", "inactive"]);
|
|
@@ -164,7 +165,7 @@ export type IdentityConfig = z.infer<typeof IdentityConfigSchema>;
|
|
|
164
165
|
// machine, never here, so a compromised sandbox can't exceed what the owner ticked.
|
|
165
166
|
// on/off, not boolean: capability configs arrive from the form as strings, and a select renders an enum.
|
|
166
167
|
const hostScope = z.enum(["on", "off"]);
|
|
167
|
-
export const
|
|
168
|
+
export const DeviceScopesSchema = z.object({
|
|
168
169
|
// Run commands in a real shell (PowerShell on Windows, the login shell on Linux). Off ⇒ files/screen only.
|
|
169
170
|
shell: hostScope.default("on"),
|
|
170
171
|
// Create, modify and trash files under `roots`. Reads are always allowed within them; this is the write half.
|
|
@@ -181,8 +182,8 @@ export const HostScopesSchema = z.object({
|
|
|
181
182
|
// One directory per line. Empty ⇒ the machine's home directory, which is what the agent reports at connect.
|
|
182
183
|
roots: z.string().optional(),
|
|
183
184
|
});
|
|
184
|
-
export type
|
|
185
|
-
export const
|
|
185
|
+
export type DeviceScopes = z.infer<typeof DeviceScopesSchema>;
|
|
186
|
+
export const DeviceConfigSchema = DeviceScopesSchema.extend({ platform: z.string().min(1) });
|
|
186
187
|
// An ACP agent served as a chat provider: the daemon spawns `command` as a long-lived JSON-RPC subprocess, and the
|
|
187
188
|
// capability id becomes the provider id. `env` is a pasted KEY=VALUE block, the whole secret field; `loginCommand` runs
|
|
188
189
|
// an interactive device-code login in a visible terminal.
|
|
@@ -287,6 +288,14 @@ export const WalletConfigSchema = z.object({
|
|
|
287
288
|
deny: z.string().optional(),
|
|
288
289
|
});
|
|
289
290
|
export type WalletConfig = z.infer<typeof WalletConfigSchema>;
|
|
291
|
+
// The owner's Intentic account, reachable from inside this sandbox: what lets it create sandboxes on that account
|
|
292
|
+
// instead of asking a person to fetch a setup code from the browser. The token is minted at Settings ▸ Tokens and is
|
|
293
|
+
// account-scoped, so it is held by the daemon and never injected into the agent's env — the approval card on every
|
|
294
|
+
// create is only an approval while the agent cannot mint a claim behind it.
|
|
295
|
+
export const FleetConfigSchema = z.object({
|
|
296
|
+
token: z.string().min(1).describe("A provisioning token from Settings ▸ Tokens. Stored, never echoed back."),
|
|
297
|
+
});
|
|
298
|
+
export type FleetConfig = z.infer<typeof FleetConfigSchema>;
|
|
290
299
|
export type McpConfig = z.infer<typeof McpConfigSchema>;
|
|
291
300
|
export type CliConfig = z.infer<typeof CliConfigSchema>;
|
|
292
301
|
export type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
@@ -294,7 +303,7 @@ export type ExtensionConfig = z.infer<typeof ExtensionConfigSchema>;
|
|
|
294
303
|
export type SshConfig = z.infer<typeof SshConfigSchema>;
|
|
295
304
|
export type DockerConfig = z.infer<typeof DockerConfigSchema>;
|
|
296
305
|
export type BrowserConfig = z.infer<typeof BrowserConfigSchema>;
|
|
297
|
-
export type
|
|
306
|
+
export type DeviceConfig = z.infer<typeof DeviceConfigSchema>;
|
|
298
307
|
export type AcpAgentConfig = z.infer<typeof AcpAgentConfigSchema>;
|
|
299
308
|
export type EndpointConfig = z.infer<typeof EndpointConfigSchema>;
|
|
300
309
|
export const CapabilitySchema = z.discriminatedUnion("kind", [
|
|
@@ -321,7 +330,7 @@ export const CapabilitySchema = z.discriminatedUnion("kind", [
|
|
|
321
330
|
z.object({ id: entryId, kind: z.literal("browser"), config: BrowserConfigSchema }),
|
|
322
331
|
// One email identity browser accounts are born from; they join it via their own `identity` field.
|
|
323
332
|
z.object({ id: entryId, kind: z.literal("identity"), config: IdentityConfigSchema }),
|
|
324
|
-
z.object({ id: entryId, kind: z.literal("
|
|
333
|
+
z.object({ id: entryId, kind: z.literal("device"), config: DeviceConfigSchema }),
|
|
325
334
|
// The user's own browser, through their installed extension; `host`'s sibling, one capability per browser. Distinct
|
|
326
335
|
// from `browser`, the sandbox's own Chromium profile: this one is the person's, already signed into everything,
|
|
327
336
|
// only ever borrowed.
|
|
@@ -335,6 +344,8 @@ export const CapabilitySchema = z.discriminatedUnion("kind", [
|
|
|
335
344
|
z.object({ id: entryId, kind: z.literal("localmodel"), config: LocalModelConfigSchema }),
|
|
336
345
|
// The sandbox's USDC wallet (WalletConfigSchema), one per sandbox; the key never enters the container.
|
|
337
346
|
z.object({ id: entryId, kind: z.literal("wallet"), config: WalletConfigSchema }),
|
|
347
|
+
// The owner's account, one per sandbox: the box holding this may provision sandboxes on it.
|
|
348
|
+
z.object({ id: entryId, kind: z.literal("fleet"), config: FleetConfigSchema }),
|
|
338
349
|
]);
|
|
339
350
|
export type Capability = z.infer<typeof CapabilitySchema>;
|
|
340
351
|
// A credential the owner must type elsewhere (a WhatsApp pairing code); rendered large and copyable, not buried in
|
|
@@ -365,7 +376,7 @@ export const CapabilitySummarySchema = z.object({
|
|
|
365
376
|
// by catalog card, not kind, since several cards share one kind. Re-derived on every read, so it drops out once its
|
|
366
377
|
// evidence moves; `prefill` is never a secret, even one sitting in a checked-in file.
|
|
367
378
|
export const CapabilityRecommendationSchema = z.object({
|
|
368
|
-
|
|
379
|
+
entry: z.string().describe("Which catalog entry is being suggested."),
|
|
369
380
|
evidence: z
|
|
370
381
|
.string()
|
|
371
382
|
.describe("What was seen that prompted it: a file, a remote, printed verbatim so the claim can be checked rather than believed."),
|
|
@@ -398,9 +409,9 @@ export const CapabilityConnectionSchema = z.object({
|
|
|
398
409
|
.describe("Its settings exactly as stored, credentials included. The field names are its own kind's, which the caller already knows."),
|
|
399
410
|
});
|
|
400
411
|
export type CapabilityConnection = z.infer<typeof CapabilityConnectionSchema>;
|
|
401
|
-
// DELETE /capabilities/recommendations/{
|
|
412
|
+
// DELETE /capabilities/recommendations/{entry}: the declined evidence is recorded daemon-side, so the suggestion returns
|
|
402
413
|
// once the workspace changes under it.
|
|
403
|
-
export const
|
|
414
|
+
export const CapabilityEntryParamSchema = z.object({ entry: z.string().describe("Which suggestion to stop making.") });
|
|
404
415
|
// POST /capabilities/{id}/secret: replaces just the per-kind secret field and re-runs the capability's idempotent
|
|
405
416
|
// apply.
|
|
406
417
|
export const CapabilitySecretInputSchema = z.object({
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AgentProviderSchema } from "./agent.js";
|
|
3
|
+
import { entryId } from "./internal.js";
|
|
4
|
+
|
|
5
|
+
// One reading, asked once per chat on the message it opens with, answering whichever of two questions the chat still
|
|
6
|
+
// has: which persona handles it, and what it runs on. Both are the same classification over lists the sandbox supplies,
|
|
7
|
+
// both are only answerable before anything has run, and neither is worth a model call of its own — so there is one
|
|
8
|
+
// call, and `model`/`persona` on the ask say which halves it is being paid to answer.
|
|
9
|
+
|
|
10
|
+
export const ChatRouteAskSchema = z.object({
|
|
11
|
+
prompt: z.string().min(1).max(20000).describe("The message a new chat is about to open with."),
|
|
12
|
+
paths: z
|
|
13
|
+
.array(z.string().min(1).max(500))
|
|
14
|
+
.max(50)
|
|
15
|
+
.default([])
|
|
16
|
+
.describe("Workspace paths the message names: uploads, @-mentions, the editor's own file. How much real code the work touches, and which persona's ground it stands on."),
|
|
17
|
+
folder: z.string().max(200).optional().describe("The workspace folder the chat was opened in, when it was opened in one."),
|
|
18
|
+
editorContext: z.boolean().optional().describe("Whether the message carries a file and selection the user pointed at, so it is about real code."),
|
|
19
|
+
planMode: z.boolean().optional().describe("Whether the chat opens in plan mode, which is a request to think before acting."),
|
|
20
|
+
// The two halves, each a fact about THIS chat rather than a setting: a chat already pointed at a model or a persona
|
|
21
|
+
// by hand has nothing left to ask about it, whatever the sandbox's settings say.
|
|
22
|
+
model: z.boolean().describe("Whether to choose the model, effort and account: true when the chat is on Auto and nothing has been picked by hand."),
|
|
23
|
+
persona: z.boolean().describe("Whether to choose the persona: true when persona matching is on and the chat has not been pointed at one by hand."),
|
|
24
|
+
});
|
|
25
|
+
export type ChatRouteAsk = z.infer<typeof ChatRouteAskSchema>;
|
|
26
|
+
|
|
27
|
+
export const ModelPickSchema = z.object({
|
|
28
|
+
provider: AgentProviderSchema.describe("Which provider serves the conversation."),
|
|
29
|
+
model: z.string().min(1).describe("Which of its models."),
|
|
30
|
+
effort: z.string().optional().describe("How hard it should think, where the model offers a choice. Absent takes the model's own default."),
|
|
31
|
+
account: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Which connected account pays, by its daemon-minted id. Absent leaves it to whichever account has the most headroom."),
|
|
35
|
+
});
|
|
36
|
+
export type ModelPick = z.infer<typeof ModelPickSchema>;
|
|
37
|
+
|
|
38
|
+
// Absent `id` is a real, safe answer: the chat stays open to every account, and the reason says why nothing was named.
|
|
39
|
+
export const PersonaVerdictSchema = z.object({
|
|
40
|
+
id: entryId.optional().describe("The persona this message belongs to, or absent when none does."),
|
|
41
|
+
reason: z.string().describe("Why, in the one clause a chat can show. Present whether or not a persona was named."),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Absent `pick` is a real, safe answer: the chat runs on whatever the picker remembered, and the reason says why.
|
|
45
|
+
export const ModelVerdictSchema = z.object({
|
|
46
|
+
pick: ModelPickSchema.optional().describe("What the conversation should run on, or absent when nothing could be chosen and the usual pick stands."),
|
|
47
|
+
reason: z.string().describe("Why, in the one clause a chat can show. Present whether or not a model was named."),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// One verdict per half asked for; a half that wasn't asked is absent, never a verdict with an empty answer.
|
|
51
|
+
export const ChatRouteSchema = z.object({
|
|
52
|
+
persona: PersonaVerdictSchema.optional().describe("The persona half's answer, present only when it was asked for."),
|
|
53
|
+
model: ModelVerdictSchema.optional().describe("The model half's answer, present only when it was asked for."),
|
|
54
|
+
// Absent means nothing was spent: a folder match, the only runnable model, an unset role, a spent chain, or the
|
|
55
|
+
// deadline passing before any rung answered.
|
|
56
|
+
judge: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Which model answered, as `provider:model`, so the chat can name what the reading cost. Absent when no model was reached at all."),
|
|
60
|
+
});
|
|
61
|
+
export type ChatRoute = z.infer<typeof ChatRouteSchema>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { contextTrimLine } from "./context-trim.js";
|
|
3
|
+
|
|
4
|
+
// The one thing a reader ever sees of this mechanism. It has to answer three questions in one line: why the turn ran
|
|
5
|
+
// thin, what it lost, and whether the reader's own instructions were among the losses.
|
|
6
|
+
|
|
7
|
+
test("the window comes first, then what went, then what stayed", () => {
|
|
8
|
+
const line = contextTrimLine({ window: 16_384, omitted: ["Map of this project", "How this sandbox asks agents to work"], base: true });
|
|
9
|
+
|
|
10
|
+
expect(line).toContain("16k window");
|
|
11
|
+
expect(line).toContain("Map of this project, How this sandbox asks agents to work");
|
|
12
|
+
// Without this clause the line reads as "your AGENTS.md may not have arrived", which is the one thing it is not.
|
|
13
|
+
expect(line).toContain("Your workspace rules");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// A swapped base is a bigger change than any note and is stated as its own fact, not folded into the list.
|
|
17
|
+
test("a swapped base says so, and a kept one does not", () => {
|
|
18
|
+
const swapped = contextTrimLine({ window: 16_384, omitted: [], base: true });
|
|
19
|
+
const kept = contextTrimLine({ window: 32_768, omitted: ["Map of this project"], base: false });
|
|
20
|
+
|
|
21
|
+
expect(swapped).toContain("base instructions were swapped");
|
|
22
|
+
expect(kept).not.toContain("base instructions");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Nothing to list happens for real: a turn whose optional notes were all off already, on a window small enough to swap
|
|
26
|
+
// the base. The sentence must not trail off into an empty list.
|
|
27
|
+
test("an empty list leaves no dangling clause", () => {
|
|
28
|
+
const line = contextTrimLine({ window: 16_384, omitted: [], base: true });
|
|
29
|
+
|
|
30
|
+
expect(line).toContain("16k window.");
|
|
31
|
+
expect(line).not.toContain("left out");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// A user's own server can be started with any number at all, and rounding it to "24k" would misquote the thing the
|
|
35
|
+
// reader has to go and change.
|
|
36
|
+
test("a window that is not a round number is spelled out", () => {
|
|
37
|
+
expect(contextTrimLine({ window: 24_000, omitted: ["Map of this project"], base: false })).toContain("24,000 window");
|
|
38
|
+
});
|