@intentic/sandbox-contract 1.240.0 → 1.243.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 +13 -0
- package/dist/plan-pools.d.ts.map +1 -0
- package/dist/plan-pools.js +31 -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 +40 -5
- package/dist/schemas/computers.d.ts.map +1 -1
- package/dist/schemas/computers.js +16 -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 +92 -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 +91 -15
- 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
package/src/history-state.ts
CHANGED
|
@@ -105,6 +105,12 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
105
105
|
* export would pack THAT, each one a multiple of the last. Living on this volume is the other half of the
|
|
106
106
|
* same guard; under `/work` the file would also be watched, indexed by iq, and snapshotted into history. */
|
|
107
107
|
{ path: "exports/", portability: "derived" },
|
|
108
|
+
/* The other end of the same volume: a bundle being taken IN, spooled here while its owner reads the plan
|
|
109
|
+
* it produced. `derived` for the export directory's reason and one more — this is somebody else's bundle,
|
|
110
|
+
* mid-review, and packing a half-reviewed arrival into an export would carry a sandbox that was never
|
|
111
|
+
* this one. The pipeline deletes each spool on apply or abandon, and boot sweeps whatever a crash left
|
|
112
|
+
* (portability/bundle-arrival.ts). */
|
|
113
|
+
{ path: "arrivals/", portability: "derived" },
|
|
108
114
|
|
|
109
115
|
/* ---- credentials ---- */
|
|
110
116
|
|
package/src/index.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { contractRoutes, requestPathFor, routeForProcedure, routeNameForRequest,
|
|
|
3
3
|
import { activityContract } from "./contracts/activity.contract.js";
|
|
4
4
|
import { agentContract } from "./contracts/agent.contract.js";
|
|
5
5
|
import { agentsContract } from "./contracts/agents.contract.js";
|
|
6
|
+
import { approvalsContract } from "./contracts/approvals.contract.js";
|
|
6
7
|
import { automationsContract } from "./contracts/automations.contract.js";
|
|
7
8
|
import { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
8
9
|
import { choresContract } from "./contracts/chores.contract.js";
|
|
9
10
|
import { ciContract } from "./contracts/ci.contract.js";
|
|
10
11
|
import { claudeContract } from "./contracts/claude.contract.js";
|
|
11
12
|
import { cursorContract } from "./contracts/cursor.contract.js";
|
|
12
|
-
import { draftsContract } from "./contracts/drafts.contract.js";
|
|
13
13
|
import { endpointsContract } from "./contracts/endpoints.contract.js";
|
|
14
14
|
import { exitContract } from "./contracts/exit.contract.js";
|
|
15
15
|
import { extensionsContract } from "./contracts/extensions.contract.js";
|
|
@@ -26,8 +26,10 @@ import { panelsContract } from "./contracts/panels.contract.js";
|
|
|
26
26
|
import { portsContract } from "./contracts/ports.contract.js";
|
|
27
27
|
import { publicContract } from "./contracts/public.contract.js";
|
|
28
28
|
import { prepushContract } from "./contracts/prepush.contract.js";
|
|
29
|
+
import { keysContract } from "./contracts/keys.contract.js";
|
|
29
30
|
import { providersContract } from "./contracts/providers.contract.js";
|
|
30
31
|
import { pushContract } from "./contracts/push.contract.js";
|
|
32
|
+
import { safetyContract } from "./contracts/safety.contract.js";
|
|
31
33
|
import { secretsContract } from "./contracts/secrets.contract.js";
|
|
32
34
|
import { sessionsContract } from "./contracts/sessions.contract.js";
|
|
33
35
|
import { settingsContract } from "./contracts/settings.contract.js";
|
|
@@ -43,13 +45,13 @@ import { workspaceContract } from "./contracts/workspace.contract.js";
|
|
|
43
45
|
export { activityContract } from "./contracts/activity.contract.js";
|
|
44
46
|
export { agentContract } from "./contracts/agent.contract.js";
|
|
45
47
|
export { agentsContract } from "./contracts/agents.contract.js";
|
|
48
|
+
export { approvalsContract } from "./contracts/approvals.contract.js";
|
|
46
49
|
export { automationsContract } from "./contracts/automations.contract.js";
|
|
47
50
|
export { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
48
51
|
export { choresContract } from "./contracts/chores.contract.js";
|
|
49
52
|
export { ciContract } from "./contracts/ci.contract.js";
|
|
50
53
|
export { claudeContract } from "./contracts/claude.contract.js";
|
|
51
54
|
export { cursorContract } from "./contracts/cursor.contract.js";
|
|
52
|
-
export { draftsContract } from "./contracts/drafts.contract.js";
|
|
53
55
|
export { endpointsContract, type TrialHealth, TrialStatusSchema, type TrialStatusResponse } from "./contracts/endpoints.contract.js";
|
|
54
56
|
export { exitContract } from "./contracts/exit.contract.js";
|
|
55
57
|
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
@@ -75,8 +77,10 @@ export { panelsContract } from "./contracts/panels.contract.js";
|
|
|
75
77
|
export { portsContract } from "./contracts/ports.contract.js";
|
|
76
78
|
export { publicContract } from "./contracts/public.contract.js";
|
|
77
79
|
export { prepushContract } from "./contracts/prepush.contract.js";
|
|
80
|
+
export { keysContract } from "./contracts/keys.contract.js";
|
|
78
81
|
export { providersContract } from "./contracts/providers.contract.js";
|
|
79
82
|
export { pushContract } from "./contracts/push.contract.js";
|
|
83
|
+
export { safetyContract } from "./contracts/safety.contract.js";
|
|
80
84
|
export { secretsContract } from "./contracts/secrets.contract.js";
|
|
81
85
|
export { sessionsContract } from "./contracts/sessions.contract.js";
|
|
82
86
|
export { settingsContract } from "./contracts/settings.contract.js";
|
|
@@ -89,6 +93,8 @@ export { vpnContract } from "./contracts/vpn.contract.js";
|
|
|
89
93
|
export { workflowsContract } from "./contracts/workflows.contract.js";
|
|
90
94
|
export { workspaceContract } from "./contracts/workspace.contract.js";
|
|
91
95
|
export * from "./events.js";
|
|
96
|
+
export * from "./card-status.js";
|
|
97
|
+
export * from "./mentions.js";
|
|
92
98
|
export * from "./sse.js";
|
|
93
99
|
export * from "./routes.js";
|
|
94
100
|
/* THE CONTAINER'S FIXED DIRECTORY LAYOUT, re-exported so extensions can reach it.
|
|
@@ -106,10 +112,18 @@ export * from "./workspace-state.js";
|
|
|
106
112
|
export * from "./runtime-state.js";
|
|
107
113
|
export * from "./state-portability.js";
|
|
108
114
|
export * from "./history-state.js";
|
|
115
|
+
/* THE PROVIDER VOCABULARY, bottom-up: what each agentic loop can do (agent-runtimes), the one row-per-provider
|
|
116
|
+
* table built on those records (provider-specs), and the shapes each surface reads that table in
|
|
117
|
+
* (agent-catalog). Exported as three modules rather than folded into one, because the arrow between them only
|
|
118
|
+
* points one way and saying so here is what stops it curling back. */
|
|
119
|
+
export * from "./agent-runtimes.js";
|
|
120
|
+
export * from "./provider-specs.js";
|
|
109
121
|
export * from "./agent-catalog.js";
|
|
110
122
|
export * from "./agent-run-model.js";
|
|
111
123
|
export * from "./capability-env.js";
|
|
112
124
|
export * from "./command-classes.js";
|
|
125
|
+
export * from "./command-run.js";
|
|
126
|
+
export * from "./safety-policy.js";
|
|
113
127
|
export * from "./credential-material.js";
|
|
114
128
|
export * from "./capability-secrets.js";
|
|
115
129
|
export * from "./conversation-ids.js";
|
|
@@ -121,12 +135,14 @@ export * from "./webext-links.js";
|
|
|
121
135
|
export * from "./runner-protocol.js";
|
|
122
136
|
export * from "./listener-protocol.js";
|
|
123
137
|
export * from "./hostnames.js";
|
|
138
|
+
export * from "./overlay-lint.js";
|
|
124
139
|
export * from "./model-order.js";
|
|
140
|
+
export * from "./plan-pools.js";
|
|
125
141
|
export * from "./path-refs.js";
|
|
126
142
|
export * from "./prompt-complexity.js";
|
|
127
143
|
export * from "./quick-model.js";
|
|
128
144
|
export * from "./output-fields.js";
|
|
129
|
-
export * from "./
|
|
145
|
+
export * from "./approvals-execution.js";
|
|
130
146
|
/* The wire shapes, one module per subject area, mirroring `contracts/` above: every request and response the
|
|
131
147
|
* daemon speaks is declared in exactly one of them. Inputs that carry a `{param}` in their route path (repo /
|
|
132
148
|
* id / name) merge the path param into the same flat object, oRPC fills the path placeholder from the matching
|
|
@@ -137,13 +153,13 @@ export * from "./publish-drafts.js";
|
|
|
137
153
|
export * from "./schemas/activity.js";
|
|
138
154
|
export * from "./schemas/agent.js";
|
|
139
155
|
export * from "./schemas/agents.js";
|
|
156
|
+
export * from "./schemas/approvals.js";
|
|
140
157
|
export * from "./schemas/automations.js";
|
|
141
158
|
export * from "./schemas/capabilities.js";
|
|
142
159
|
export * from "./schemas/ci.js";
|
|
143
160
|
export * from "./schemas/claude-gate.js";
|
|
144
161
|
export * from "./schemas/codebase-health.js";
|
|
145
162
|
export * from "./schemas/computers.js";
|
|
146
|
-
export * from "./schemas/drafts.js";
|
|
147
163
|
export * from "./schemas/engines.js";
|
|
148
164
|
export * from "./schemas/environment.js";
|
|
149
165
|
export * from "./schemas/exit.js";
|
|
@@ -183,6 +199,7 @@ export * from "./schemas/workspace-repos.js";
|
|
|
183
199
|
export * from "./schemas/workspace-search.js";
|
|
184
200
|
export * from "./schemas/workspace-setup.js";
|
|
185
201
|
export * from "./schemas/workspace-tree.js";
|
|
202
|
+
export * from "./arrival.js";
|
|
186
203
|
export * from "./definition.js";
|
|
187
204
|
export * from "./search-globs.js";
|
|
188
205
|
export * from "./starter.js";
|
|
@@ -198,16 +215,17 @@ export const sandboxContract = {
|
|
|
198
215
|
activity: activityContract,
|
|
199
216
|
agent: agentContract,
|
|
200
217
|
agents: agentsContract,
|
|
218
|
+
approvals: approvalsContract,
|
|
201
219
|
automations: automationsContract,
|
|
202
220
|
capabilities: capabilitiesContract,
|
|
203
221
|
chores: choresContract,
|
|
204
222
|
ci: ciContract,
|
|
205
223
|
claude: claudeContract,
|
|
206
224
|
cursor: cursorContract,
|
|
207
|
-
drafts: draftsContract,
|
|
208
225
|
endpoints: endpointsContract,
|
|
209
226
|
extensions: extensionsContract,
|
|
210
227
|
personas: personasContract,
|
|
228
|
+
safety: safetyContract,
|
|
211
229
|
sessions: sessionsContract,
|
|
212
230
|
settings: settingsContract,
|
|
213
231
|
share: shareContract,
|
|
@@ -225,6 +243,7 @@ export const sandboxContract = {
|
|
|
225
243
|
ports: portsContract,
|
|
226
244
|
public: publicContract,
|
|
227
245
|
prepush: prepushContract,
|
|
246
|
+
keys: keysContract,
|
|
228
247
|
providers: providersContract,
|
|
229
248
|
push: pushContract,
|
|
230
249
|
secrets: secretsContract,
|
package/src/ingress-protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ServerHttp2Stream,
|
|
9
9
|
} from "node:http2";
|
|
10
10
|
import { type AddressInfo, createServer as createNetServer, connect as netConnect, type Socket } from "node:net";
|
|
11
|
-
import
|
|
11
|
+
import { Duplex } from "node:stream";
|
|
12
12
|
|
|
13
13
|
/* THE DATA PLANE OF THE INGRESS TUNNEL: both halves of it, over any node Duplex, in node core alone.
|
|
14
14
|
*
|
|
@@ -572,3 +572,54 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
|
|
|
572
572
|
},
|
|
573
573
|
};
|
|
574
574
|
};
|
|
575
|
+
|
|
576
|
+
/* ── THE WEBSOCKET, AS A DUPLEX ──────────────────────────────────────────────────────────────────────────
|
|
577
|
+
*
|
|
578
|
+
* `ws` ships this as `createWebSocketStream`, and BUN DOES NOT IMPLEMENT IT: the call throws
|
|
579
|
+
* `Error("Not supported yet in Bun")` out of ws's `Receiver` constructor (verified on 1.3.14, on 1.4.0, and on
|
|
580
|
+
* 1.4.1). The edge runs on Bun, and the throw landed inside its `upgrade` handler with no
|
|
581
|
+
* `try` of ours on the stack — so the FIRST sandbox to register a tunnel killed the process, taking every
|
|
582
|
+
* other sandbox's tunnel with it, and the container restarted straight into the same crash when that
|
|
583
|
+
* sandbox retried. It is the reason a hosted sandbox could be provisioned and then never come up.
|
|
584
|
+
*
|
|
585
|
+
* Reimplementing it is a dozen lines and buys more than the fix: both halves of the tunnel now get the same
|
|
586
|
+
* bytes on Bun and on node, so the runtime stops being a variable on the hot path. It belongs in this file
|
|
587
|
+
* because this file already owns both halves and the property below is one of its stated invariants.
|
|
588
|
+
*
|
|
589
|
+
* THE PROPERTY TO PRESERVE is the BYTE SAFETY note at the top: the write callback fires only once the socket
|
|
590
|
+
* has taken the frame (`send(chunk, …, callback)`), so the stream owns each chunk until it is out and no
|
|
591
|
+
* pooled Buffer is ever handed to an asynchronous writer. `read()` is a no-op because the socket pushes —
|
|
592
|
+
* there is nothing to pull — and a closed socket ends the readable side with `push(null)` so an h2 session
|
|
593
|
+
* over it sees a clean EOF rather than a hang. */
|
|
594
|
+
export interface TunnelWebSocket {
|
|
595
|
+
readonly send: (data: Buffer, options: { binary: boolean }, callback: (error?: Error) => void) => void;
|
|
596
|
+
readonly close: () => void;
|
|
597
|
+
readonly on: {
|
|
598
|
+
(event: `message`, listener: (data: unknown) => void): unknown;
|
|
599
|
+
(event: `close`, listener: () => void): unknown;
|
|
600
|
+
(event: `error`, listener: (error: Error) => void): unknown;
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export const webSocketDuplex = (socket: TunnelWebSocket): Duplex => {
|
|
605
|
+
const duplex = new Duplex({
|
|
606
|
+
read: () => {},
|
|
607
|
+
write: (chunk: Buffer, _encoding, callback) => {
|
|
608
|
+
socket.send(chunk, { binary: true }, (error) => callback(error ?? null));
|
|
609
|
+
},
|
|
610
|
+
// Half-closing a WebSocket is not a thing: ending the writable side ends the socket, which is what
|
|
611
|
+
// every caller here means by it (a session that has written its GOAWAY is done with the transport).
|
|
612
|
+
final: (callback) => {
|
|
613
|
+
socket.close();
|
|
614
|
+
callback();
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
// `message` carries a Buffer for binary frames under both ws and Bun; a text frame (nothing on this
|
|
618
|
+
// protocol sends one) would arrive as a string, and coercing it is cheaper than dropping bytes silently.
|
|
619
|
+
socket.on(`message`, (data: unknown) => {
|
|
620
|
+
duplex.push(Buffer.isBuffer(data) ? data : Buffer.from(data as string));
|
|
621
|
+
});
|
|
622
|
+
socket.on(`close`, () => duplex.push(null));
|
|
623
|
+
socket.on(`error`, (error: Error) => duplex.destroy(error));
|
|
624
|
+
return duplex;
|
|
625
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mentionedPathTokens, mentionPaths } from "./mentions.js";
|
|
3
|
+
|
|
4
|
+
describe(`mentionPaths`, () => {
|
|
5
|
+
it(`extracts path-looking tokens, deduped, with trailing punctuation stripped`, () => {
|
|
6
|
+
expect(mentionPaths(`see @src/app.ts and @readme.md, plus @src/app.ts again`)).toEqual([`src/app.ts`, `readme.md`]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it(`skips prose handles and mid-word @`, () => {
|
|
10
|
+
expect(mentionPaths(`thanks @radarsu — mail me@example.com`)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it(`skips scoped package script prefixes in copied pnpm output`, () => {
|
|
14
|
+
expect(mentionPaths(`@intentic/iq-engine:test: failed\nsee @src/app.ts`)).toEqual([`src/app.ts`]);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe(`mentionedPathTokens`, () => {
|
|
19
|
+
it(`keeps the package-script shape the composer refuses, so a transcript can still recognise it inline`, () => {
|
|
20
|
+
expect(mentionedPathTokens(`@intentic/iq-engine:test: failed\nsee @src/app.ts`)).toEqual([`intentic/iq-engine:test`, `src/app.ts`]);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/mentions.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* @-FILE MENTIONS, the two readers of the same text: the composer, which sends every path a message names as
|
|
2
|
+
* an attachment (the daemon resolves workspace-relative paths and folds them into the prompt as a Read-tool
|
|
3
|
+
* note, no upload involved), and the fold, which draws a message's UPLOADED files as chips and its inline
|
|
4
|
+
* mentions as nothing at all, because they are already visible in the words. One tokenizer, so what the
|
|
5
|
+
* composer counts as a mention is what the transcript declines to redraw. */
|
|
6
|
+
|
|
7
|
+
// Every path-looking @ token in text, including tokens that are not valid composer mentions. Broad on purpose:
|
|
8
|
+
// old turns persisted these candidates in the shared attachment field, and the fold needs the broad set to
|
|
9
|
+
// recognise and hide those inline paths rather than redraw them as file chips.
|
|
10
|
+
export const mentionedPathTokens = (text: string): string[] => {
|
|
11
|
+
const paths = new Set<string>();
|
|
12
|
+
for (const match of text.matchAll(/(?:^|\s)@([^\s@]+)/g)) {
|
|
13
|
+
const token = (match[1] as string).replace(/[.,;:!?)]+$/, ``);
|
|
14
|
+
if (token.includes(`/`) || token.includes(`.`)) {
|
|
15
|
+
paths.add(token);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...paths];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Workspace paths referenced as @-mentions in a prompt, deduped. A scoped package script prefix from copied
|
|
22
|
+
// pnpm output has the same opening shape (`@scope/package:test:`) but is not a file; accepting it hands the
|
|
23
|
+
// daemon a phantom attachment which only becomes visible when a transcript redraws the wire fields.
|
|
24
|
+
const PACKAGE_SCRIPT = /^[^/]+\/[^/]+:[^/]+$/u;
|
|
25
|
+
export const mentionPaths = (text: string): string[] => mentionedPathTokens(text).filter((token) => !PACKAGE_SCRIPT.test(token));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/* ONE "DO THIS BY HAND" LINE, the honesty unit three surfaces share: what an arrival could not do for the
|
|
4
|
+
* owner (arrival.ts), what a derived definition could not express (definition.ts), and where a runner has
|
|
5
|
+
* drifted from its parent (runner-protocol.ts). A subject the UI bolds, a detail written as an instruction.
|
|
6
|
+
*
|
|
7
|
+
* It lives in its own leaf module because runner-protocol.ts speaks it, and THAT file is imported by
|
|
8
|
+
* schemas/agent.ts, which definition.ts imports — the schema living in definition.ts made a cycle that left
|
|
9
|
+
* schemas half-initialized under module-eval.
|
|
10
|
+
*
|
|
11
|
+
* The name says the FIELD it fills (`needsAction`) rather than any one surface, because naming it for the
|
|
12
|
+
* definition is what let the arrival surfaces drift apart in the first place. */
|
|
13
|
+
export const NeedsActionSchema = z.object({ subject: z.string(), detail: z.string() });
|
|
14
|
+
export type NeedsAction = z.infer<typeof NeedsActionSchema>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { hasOfficialBase, invalidExtensionFragment, isOfficialSandboxImage, lintOverlay, overlayBase, rewriteOverlayBase } from "./overlay-lint.js";
|
|
3
|
+
|
|
4
|
+
test("accepts a RUN/ENV-only fragment, comments and blank lines included", () => {
|
|
5
|
+
const fragment = `# install the postgres client\nRUN apt-get update && apt-get install -y postgresql-client\nENV PGCLIENT=1\n`;
|
|
6
|
+
expect(invalidExtensionFragment(fragment)).toBeUndefined();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("accepts a line-continued RUN body", () => {
|
|
10
|
+
const fragment = `RUN set -eux; \\\n apt-get update; \\\n apt-get install -y whisper\n`;
|
|
11
|
+
expect(invalidExtensionFragment(fragment)).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("rejects FROM in a fragment (the daemon owns the base pin)", () => {
|
|
15
|
+
expect(invalidExtensionFragment(`FROM ubuntu:24.04\nRUN echo hi`)).toBe(`FROM ubuntu:24.04`);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("rejects a non-RUN/ENV instruction in a fragment", () => {
|
|
19
|
+
expect(invalidExtensionFragment(`RUN echo ok\nCOPY x /x`)).toBe(`COPY x /x`);
|
|
20
|
+
expect(invalidExtensionFragment(`USER root`)).toBe(`USER root`);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("rejects a privileged runtime directive, even hidden in a comment or a continued body", () => {
|
|
24
|
+
expect(invalidExtensionFragment(`# intentic:runtime --privileged`)).toBe(`# intentic:runtime --privileged`);
|
|
25
|
+
expect(invalidExtensionFragment(`RUN true \\\n # intentic:runtime --cap-add=NET_ADMIN`)).toBe(` # intentic:runtime --cap-add=NET_ADMIN`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("the official image is the published sandbox under any tag", () => {
|
|
29
|
+
expect(isOfficialSandboxImage(`ghcr.io/intentic/sandbox:stable`)).toBe(true);
|
|
30
|
+
expect(isOfficialSandboxImage(`ghcr.io/intentic/sandbox:1.52.0`)).toBe(true);
|
|
31
|
+
expect(isOfficialSandboxImage(`ghcr.io/intentic/sandbox:`)).toBe(false);
|
|
32
|
+
expect(isOfficialSandboxImage(`intentic-sandbox:dev`)).toBe(false);
|
|
33
|
+
expect(isOfficialSandboxImage(`ghcr.io/intentic/sandbox:stable extra`)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("overlayBase reads the first instruction's FROM and nothing else", () => {
|
|
37
|
+
expect(overlayBase(`# composed\n\nFROM ghcr.io/intentic/sandbox:stable\nRUN true\n`)).toBe(`ghcr.io/intentic/sandbox:stable`);
|
|
38
|
+
expect(overlayBase(`RUN true\nFROM ghcr.io/intentic/sandbox:stable\n`)).toBeUndefined();
|
|
39
|
+
expect(overlayBase(``)).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("hasOfficialBase pins the first instruction to the official sandbox image", () => {
|
|
43
|
+
expect(hasOfficialBase("FROM ghcr.io/intentic/sandbox:stable\nRUN true\n")).toBe(true);
|
|
44
|
+
expect(hasOfficialBase("# comment\n\nFROM ghcr.io/intentic/sandbox:1.52.0\nRUN true\n")).toBe(true);
|
|
45
|
+
expect(hasOfficialBase("FROM alpine:latest\n")).toBe(false);
|
|
46
|
+
expect(hasOfficialBase("FROM ghcr.io/intentic/sandbox:\n")).toBe(false);
|
|
47
|
+
expect(hasOfficialBase("RUN true\nFROM ghcr.io/intentic/sandbox:stable\n")).toBe(false);
|
|
48
|
+
expect(hasOfficialBase("")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const COMPOSED =
|
|
52
|
+
`# Composed by the intentic sandbox daemon: do not edit by hand.\n\n` +
|
|
53
|
+
`FROM ghcr.io/intentic/sandbox:stable\n\n` +
|
|
54
|
+
`# docker capability: this directive grants dockerd the privileges it needs\n` +
|
|
55
|
+
`# intentic:runtime --privileged\n\n` +
|
|
56
|
+
`# ---- custom (owner-approved) ----\n` +
|
|
57
|
+
`RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\\n` +
|
|
58
|
+
` apt-get update && apt-get install -y --no-install-recommends gnucobol\n` +
|
|
59
|
+
`ENV COBOL=1\n`;
|
|
60
|
+
|
|
61
|
+
test("lintOverlay accepts a composed overlay: official FROM, runtime directive comments, RUN/ENV", () => {
|
|
62
|
+
expect(lintOverlay(COMPOSED)).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("lintOverlay names the line that breaks the grammar", () => {
|
|
66
|
+
expect(lintOverlay(`FROM alpine:3.20\nRUN true\n`)).toBe(`FROM alpine:3.20`);
|
|
67
|
+
expect(lintOverlay(`RUN true\nFROM ghcr.io/intentic/sandbox:stable\n`)).toBe(`RUN true`);
|
|
68
|
+
expect(lintOverlay(`FROM ghcr.io/intentic/sandbox:stable\nCOPY . /work\n`)).toBe(`COPY . /work`);
|
|
69
|
+
expect(lintOverlay(`FROM ghcr.io/intentic/sandbox:stable\nRUN true\nFROM ghcr.io/intentic/sandbox:beta\n`)).toBe(
|
|
70
|
+
`FROM ghcr.io/intentic/sandbox:beta`,
|
|
71
|
+
);
|
|
72
|
+
expect(lintOverlay(`FROM ghcr.io/intentic/sandbox:stable\nUSER nobody\n`)).toBe(`USER nobody`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("lintOverlay refuses an overlay with nothing to build", () => {
|
|
76
|
+
expect(lintOverlay(``)).toBe(``);
|
|
77
|
+
expect(lintOverlay(`# only a comment\n`)).toBe(``);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("rewriteOverlayBase changes the first FROM and nothing else", () => {
|
|
81
|
+
const rewritten = rewriteOverlayBase(COMPOSED, `ghcr.io/intentic/sandbox:1.53.0`);
|
|
82
|
+
expect(overlayBase(rewritten)).toBe(`ghcr.io/intentic/sandbox:1.53.0`);
|
|
83
|
+
expect(rewritten.replace(`FROM ghcr.io/intentic/sandbox:1.53.0`, `FROM ghcr.io/intentic/sandbox:stable`)).toBe(COMPOSED);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("rewriteOverlayBase leaves content alone when the first instruction is not a FROM", () => {
|
|
87
|
+
expect(rewriteOverlayBase(`RUN true\n`, `ghcr.io/intentic/sandbox:stable`)).toBe(`RUN true\n`);
|
|
88
|
+
expect(rewriteOverlayBase(``, `ghcr.io/intentic/sandbox:stable`)).toBe(``);
|
|
89
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/* WHAT AN ENVIRONMENT OVERLAY MAY SAY, read the same way by everyone who handles one.
|
|
2
|
+
*
|
|
3
|
+
* An overlay is the Dockerfile that extends the sandbox image for one sandbox: a pinned FROM, the enabled
|
|
4
|
+
* capabilities' fragments, and the custom section the owner approved. Three readers apply the same grammar
|
|
5
|
+
* at three trust levels, and none of them may trust the one before:
|
|
6
|
+
* • the daemon admits an EXTENSION's fragment only if it is RUN/ENV (capabilities/handlers/extension.ts at
|
|
7
|
+
* install, environment/fragment-sources.ts again at compose, in case the checkout changed);
|
|
8
|
+
* • the daemon pins the composed file's first instruction to the official image (environment.ts);
|
|
9
|
+
* • the platform builds a HOSTED sandbox's overlay on a machine of its own, from content that reached it
|
|
10
|
+
* through an agent-writable volume and a browser, so it re-checks all of it before any machine exists.
|
|
11
|
+
* Text in, verdict out. No filesystem and no services, so the platform imports this the way it imports
|
|
12
|
+
* tunnel-ids, and the ic binary's Rust twin (recreate.rs `overlay_base`, runner.rs) reads the same shape. */
|
|
13
|
+
|
|
14
|
+
// The marker the rebuild executors read runtime privileges from (`# intentic:runtime --privileged`, the
|
|
15
|
+
// docker run flag it becomes is allowlisted there). A fragment that can be written by anything but the
|
|
16
|
+
// daemon's own code must not carry it, in a comment or anywhere else.
|
|
17
|
+
const RUNTIME_DIRECTIVE = "intentic:runtime";
|
|
18
|
+
|
|
19
|
+
const OFFICIAL_SANDBOX_IMAGE = /^ghcr\.io\/intentic\/sandbox:\S+$/;
|
|
20
|
+
|
|
21
|
+
// The published sandbox image under any tag, the only base an overlay may extend.
|
|
22
|
+
export const isOfficialSandboxImage = (ref: string): boolean => OFFICIAL_SANDBOX_IMAGE.test(ref);
|
|
23
|
+
|
|
24
|
+
interface OverlayLine {
|
|
25
|
+
readonly raw: string;
|
|
26
|
+
// `body` is a line inside a `\`-continued instruction, which carries no keyword of its own.
|
|
27
|
+
readonly kind: "blank" | "comment" | "instruction" | "body";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Continuation-aware: a `\` at the end of any line, comments included, makes the next line a body line.
|
|
31
|
+
const overlayLines = (content: string): OverlayLine[] => {
|
|
32
|
+
let continued = false;
|
|
33
|
+
return content.split("\n").map((raw) => {
|
|
34
|
+
const line = raw.trim();
|
|
35
|
+
const wasContinued = continued;
|
|
36
|
+
continued = line.endsWith("\\");
|
|
37
|
+
const kind = line === "" ? "blank" : line.startsWith("#") ? "comment" : wasContinued ? "body" : "instruction";
|
|
38
|
+
return { raw, kind };
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const FROM = /^from\s+(\S+)/i;
|
|
43
|
+
const RUN_OR_ENV = /^(run|env)\s/i;
|
|
44
|
+
|
|
45
|
+
/* An extension fragment may install and set variables, and nothing else. FROM is refused because the daemon
|
|
46
|
+
* owns the base pin; every other instruction because there is no build context to COPY from and nothing an
|
|
47
|
+
* extension has any business doing to USER, ENTRYPOINT or EXPOSE; and the runtime directive anywhere,
|
|
48
|
+
* because an out-of-band executor greps for it and a comment is exactly where it would hide. Answers the
|
|
49
|
+
* offending line, or undefined when the fragment is clean. */
|
|
50
|
+
export const invalidExtensionFragment = (content: string): string | undefined => {
|
|
51
|
+
for (const { raw, kind } of overlayLines(content)) {
|
|
52
|
+
if (raw.includes(RUNTIME_DIRECTIVE)) {
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
if (kind === "instruction" && !RUN_OR_ENV.test(raw.trim())) {
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// The image a composed overlay extends: the first instruction, when it is a FROM.
|
|
63
|
+
export const overlayBase = (content: string): string | undefined => {
|
|
64
|
+
const first = overlayLines(content).find((line) => line.kind === "instruction");
|
|
65
|
+
return first === undefined ? undefined : FROM.exec(first.raw.trim())?.[1];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Whether the overlay's first instruction pins it to the official sandbox image.
|
|
69
|
+
export const hasOfficialBase = (content: string): boolean => {
|
|
70
|
+
const base = overlayBase(content);
|
|
71
|
+
return base !== undefined && isOfficialSandboxImage(base);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/* A whole composed overlay, as an executor that did not compose it must read it: one leading FROM on the
|
|
75
|
+
* official image, then RUN and ENV only. Comments pass, the runtime directive included, since a composed
|
|
76
|
+
* overlay is where the daemon's own capability fragments put it and the executor that honours it (ic) reads
|
|
77
|
+
* it from there; an executor that cannot honour it (a VM is already privileged) ignores it. Answers the
|
|
78
|
+
* offending line, or undefined. An overlay with no instruction at all is refused: there is nothing to build. */
|
|
79
|
+
export const lintOverlay = (content: string): string | undefined => {
|
|
80
|
+
let first = true;
|
|
81
|
+
for (const { raw, kind } of overlayLines(content)) {
|
|
82
|
+
if (kind !== "instruction") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const line = raw.trim();
|
|
86
|
+
if (first) {
|
|
87
|
+
first = false;
|
|
88
|
+
const base = FROM.exec(line)?.[1];
|
|
89
|
+
if (base === undefined || !isOfficialSandboxImage(base)) {
|
|
90
|
+
return raw;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!RUN_OR_ENV.test(line)) {
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return first ? "" : undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/* The same overlay on a different base: the first FROM rewritten, everything else byte-identical. An
|
|
102
|
+
* executor applies an approved recipe on the base IT runs (a newer release than the one the daemon composed
|
|
103
|
+
* against, a rollback pin), and hashes the content the owner approved rather than this rewrite, so the
|
|
104
|
+
* environment hash stays the reviewed one. Content whose first instruction is not a FROM is returned as is. */
|
|
105
|
+
export const rewriteOverlayBase = (content: string, base: string): string => {
|
|
106
|
+
let done = false;
|
|
107
|
+
return overlayLines(content)
|
|
108
|
+
.map(({ raw, kind }) => {
|
|
109
|
+
if (done || kind !== "instruction") {
|
|
110
|
+
return raw;
|
|
111
|
+
}
|
|
112
|
+
done = true;
|
|
113
|
+
return FROM.test(raw.trim()) ? raw.replace(FROM, `FROM ${base}`) : raw;
|
|
114
|
+
})
|
|
115
|
+
.join("\n");
|
|
116
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { mintReachabilityGrant, verifyReachabilityGrant } from "./ingress-contract.js";
|
|
4
|
+
import { isOwnerTicket, mintOwnerTicket, OWNER_TICKET_TTL_MS, publicKeyPemOf, verifyOwnerTicket } from "./owner-ticket.js";
|
|
5
|
+
|
|
6
|
+
const pair = generateKeyPairSync("ed25519");
|
|
7
|
+
const privatePem = pair.privateKey.export({ type: "pkcs8", format: "pem" }) as string;
|
|
8
|
+
const publicPem = pair.publicKey.export({ type: "spki", format: "pem" }) as string;
|
|
9
|
+
const NOW = 1_800_000_000_000;
|
|
10
|
+
|
|
11
|
+
describe("owner ticket", () => {
|
|
12
|
+
it("round-trips the claim and expires when it says", () => {
|
|
13
|
+
const ticket = mintOwnerTicket(privatePem, { sandboxId: "0123456789ab", email: "Owner@Example.com", issuedAtMs: NOW });
|
|
14
|
+
expect(isOwnerTicket(ticket)).toBe(true);
|
|
15
|
+
expect(verifyOwnerTicket(publicPem, ticket, NOW + 1_000)).toEqual({
|
|
16
|
+
sandboxId: "0123456789ab",
|
|
17
|
+
// Lowercased at mint, the way the daemon's owner store and the platform's rows spell an address.
|
|
18
|
+
email: "owner@example.com",
|
|
19
|
+
issuedAt: Math.floor(NOW / 1000),
|
|
20
|
+
expiresAt: Math.floor((NOW + OWNER_TICKET_TTL_MS) / 1000),
|
|
21
|
+
});
|
|
22
|
+
expect(verifyOwnerTicket(publicPem, ticket, NOW + OWNER_TICKET_TTL_MS + 1_000)).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("derives the public half the machine env carries from the platform's private key", () => {
|
|
26
|
+
const ticket = mintOwnerTicket(privatePem, { sandboxId: "0123456789ab", email: "o@x.dev", issuedAtMs: NOW });
|
|
27
|
+
expect(verifyOwnerTicket(publicKeyPemOf(privatePem), ticket, NOW)).toMatchObject({ sandboxId: "0123456789ab", email: "o@x.dev" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("refuses another key's signature, a tampered claim, and garbage", () => {
|
|
31
|
+
const other = generateKeyPairSync("ed25519").publicKey.export({ type: "spki", format: "pem" }) as string;
|
|
32
|
+
const ticket = mintOwnerTicket(privatePem, { sandboxId: "0123456789ab", email: "o@x.dev", issuedAtMs: NOW });
|
|
33
|
+
expect(verifyOwnerTicket(other, ticket, NOW)).toBeUndefined();
|
|
34
|
+
const [prefix, , signature] = ticket.split(".");
|
|
35
|
+
const forged = Buffer.from(JSON.stringify({ sub: "0123456789ab", email: "thief@x.dev", iat: 1, exp: 9e9 })).toString("base64url");
|
|
36
|
+
expect(verifyOwnerTicket(publicPem, `${prefix}.${forged}.${signature}`, NOW)).toBeUndefined();
|
|
37
|
+
expect(verifyOwnerTicket(publicPem, "not a ticket", NOW)).toBeUndefined();
|
|
38
|
+
expect(isOwnerTicket("ig1.x.y")).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/* One key signs both the reachability grant and the owner ticket, and neither may ever pass as the other:
|
|
42
|
+
* a grant is a sandbox's right to serve its hostnames, a ticket is a person's right to drive it. */
|
|
43
|
+
it("is never a reachability grant, and a grant is never a ticket", () => {
|
|
44
|
+
const ticket = mintOwnerTicket(privatePem, { sandboxId: "0123456789ab", email: "o@x.dev", issuedAtMs: NOW });
|
|
45
|
+
const grant = mintReachabilityGrant(privatePem, "0123456789ab", NOW);
|
|
46
|
+
expect(verifyReachabilityGrant(publicPem, ticket)).toBeUndefined();
|
|
47
|
+
expect(verifyOwnerTicket(publicPem, grant, NOW)).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("refuses to mint for anything but a real sandbox id and an owner", () => {
|
|
51
|
+
expect(() => mintOwnerTicket(privatePem, { sandboxId: "nope", email: "o@x.dev", issuedAtMs: NOW })).toThrow();
|
|
52
|
+
expect(() => mintOwnerTicket(privatePem, { sandboxId: "0123456789ab", email: "", issuedAtMs: NOW })).toThrow();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, sign as edSign, verify as edVerify } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/* THE OWNER TICKET: the platform's signed word for who owns a HOSTED sandbox, so the browser that just signed in
|
|
4
|
+
* to the platform can sign in to that sandbox's daemon without being asked for Google a second time.
|
|
5
|
+
*
|
|
6
|
+
* THE RULE IT BENDS, AND WHY THAT IS ALLOWED HERE. Every sandbox authenticates its owner against Google itself
|
|
7
|
+
* (the daemon's auth/auth.ts): the platform never holds or forges a daemon credential, so a platform breach can
|
|
8
|
+
* read a sandbox's address and drive nothing. That rule stands for every lane but one. On the HOSTED lane the
|
|
9
|
+
* platform already creates the machine, holds its power and its disk, and injects its owner's email into its
|
|
10
|
+
* env before the daemon ever runs (ARCHITECTURE.md names this the stated exception). A platform that can already
|
|
11
|
+
* open the machine gains no new power from being able to say "this browser is the owner" to it, and the user
|
|
12
|
+
* gains the one thing the second Google prompt was costing: a sign-in that is one sign-in.
|
|
13
|
+
*
|
|
14
|
+
* So a hosted daemon, and only a hosted daemon, accepts a ticket the platform signs with the SAME Ed25519 key
|
|
15
|
+
* that signs reachability grants (ingress-contract.ts), verified offline against the public half the machine's
|
|
16
|
+
* env carries (ENV_PLATFORM_PUBLIC_KEY, set by the provisioner and by nothing else). The ticket names the
|
|
17
|
+
* sandbox (its 12-hex id, which the daemon checks against its own) and the owner's email (which the daemon
|
|
18
|
+
* checks against OWNER_EMAIL on first-bind, exactly as it checks a Google proof), and it expires in minutes:
|
|
19
|
+
* it is spent once, on the daemon's /system/session, for the same daemon-minted session a Google proof buys.
|
|
20
|
+
*
|
|
21
|
+
* A different prefix from the grant's, so neither can ever be presented as the other: a grant verifies only as
|
|
22
|
+
* a grant, a ticket only as a ticket, under one key. */
|
|
23
|
+
|
|
24
|
+
const TICKET_PREFIX = "ot1";
|
|
25
|
+
|
|
26
|
+
// Long enough for the exchange it exists for (one round trip to the daemon after the platform answers), short
|
|
27
|
+
// enough that a ticket lifted from a network log is worthless by the time anyone reads it.
|
|
28
|
+
export const OWNER_TICKET_TTL_MS = 5 * 60_000;
|
|
29
|
+
|
|
30
|
+
// The env var a hosted machine carries the platform's public key in (PEM, SPKI). Absent on every other lane,
|
|
31
|
+
// which is what keeps the ticket a hosted-only credential: a daemon with no key verifies no ticket.
|
|
32
|
+
export const ENV_PLATFORM_PUBLIC_KEY = "PLATFORM_PUBLIC_KEY";
|
|
33
|
+
|
|
34
|
+
const base64url = (bytes: Buffer): string => bytes.toString("base64url");
|
|
35
|
+
const SANDBOX_ID = /^[0-9a-f]{12}$/;
|
|
36
|
+
|
|
37
|
+
// The signed claim, times in seconds like the grant's.
|
|
38
|
+
export interface OwnerTicket {
|
|
39
|
+
readonly sandboxId: string;
|
|
40
|
+
readonly email: string;
|
|
41
|
+
readonly issuedAt: number;
|
|
42
|
+
readonly expiresAt: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const isOwnerTicket = (bearer: string): boolean => bearer.startsWith(`${TICKET_PREFIX}.`);
|
|
46
|
+
|
|
47
|
+
export const mintOwnerTicket = (
|
|
48
|
+
privateKeyPem: string,
|
|
49
|
+
claim: { readonly sandboxId: string; readonly email: string; readonly issuedAtMs: number; readonly ttlMs?: number },
|
|
50
|
+
): string => {
|
|
51
|
+
if (!SANDBOX_ID.test(claim.sandboxId)) {
|
|
52
|
+
throw new Error(`an owner ticket names a 12-hex sandbox id, got "${claim.sandboxId}"`);
|
|
53
|
+
}
|
|
54
|
+
if (claim.email === "") {
|
|
55
|
+
throw new Error("an owner ticket names an owner");
|
|
56
|
+
}
|
|
57
|
+
const iat = Math.floor(claim.issuedAtMs / 1000);
|
|
58
|
+
const exp = Math.floor((claim.issuedAtMs + (claim.ttlMs ?? OWNER_TICKET_TTL_MS)) / 1000);
|
|
59
|
+
const payload = Buffer.from(JSON.stringify({ sub: claim.sandboxId, email: claim.email.toLowerCase(), iat, exp }), "utf8");
|
|
60
|
+
const signature = edSign(null, payload, createPrivateKey(privateKeyPem));
|
|
61
|
+
return `${TICKET_PREFIX}.${base64url(payload)}.${base64url(signature)}`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/* Verify a ticket against the platform's public key, at `nowMs`. Undefined for every way of not being a valid
|
|
65
|
+
* ticket: the wrong prefix, a bad signature, a malformed claim, or one past its expiry. The CALLER checks that
|
|
66
|
+
* `sandboxId` is its own and that `email` is the owner it expects: those are the daemon's facts, not this
|
|
67
|
+
* function's. */
|
|
68
|
+
export const verifyOwnerTicket = (publicKeyPem: string, token: string, nowMs: number): OwnerTicket | undefined => {
|
|
69
|
+
const parts = token.split(".");
|
|
70
|
+
if (parts.length !== 3 || parts[0] !== TICKET_PREFIX) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const payload = Buffer.from(parts[1] as string, "base64url");
|
|
75
|
+
const signature = Buffer.from(parts[2] as string, "base64url");
|
|
76
|
+
if (!edVerify(null, payload, createPublicKey(publicKeyPem), signature)) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const parsed = JSON.parse(payload.toString("utf8")) as { sub?: unknown; email?: unknown; iat?: unknown; exp?: unknown };
|
|
80
|
+
if (
|
|
81
|
+
typeof parsed.sub !== "string" ||
|
|
82
|
+
!SANDBOX_ID.test(parsed.sub) ||
|
|
83
|
+
typeof parsed.email !== "string" ||
|
|
84
|
+
parsed.email === "" ||
|
|
85
|
+
typeof parsed.iat !== "number" ||
|
|
86
|
+
typeof parsed.exp !== "number"
|
|
87
|
+
) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.exp * 1000 <= nowMs) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return { sandboxId: parsed.sub, email: parsed.email, issuedAt: parsed.iat, expiresAt: parsed.exp };
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// The public half of the platform's signing key, in the PEM the daemon's env carries. Derived rather than
|
|
100
|
+
// configured: one key, two readers (the ingress and every hosted daemon), no second value to keep in step.
|
|
101
|
+
export const publicKeyPemOf = (privateKeyPem: string): string =>
|
|
102
|
+
createPublicKey(createPrivateKey(privateKeyPem)).export({ type: "spki", format: "pem" }) as string;
|