@intentic/sandbox-contract 1.331.0 → 1.332.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 +15 -0
- package/dist/chores/probes.d.ts.map +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/contracts/accounts.contract.d.ts +11 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +37 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +14 -3
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +29 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +2 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +7 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +2 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.d.ts +10 -0
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +7 -0
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.d.ts +70 -0
- package/dist/contracts/offload.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.js +10 -1
- package/dist/contracts/offload.contract.js.map +1 -1
- package/dist/contracts/privacy.contract.d.ts +14 -0
- package/dist/contracts/privacy.contract.d.ts.map +1 -1
- package/dist/contracts/privacy.contract.js +11 -1
- package/dist/contracts/privacy.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +3 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +103 -2
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +72 -94
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +19 -1
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +17 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +4 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +46 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +6 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +1 -1
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/resume.d.ts +3 -0
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +17 -2
- package/dist/events/resume.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +18 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +27 -1
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +2 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +64 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +17 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +313 -124
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +8 -7
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts +3 -1
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +10 -0
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/model-order.d.ts +1 -0
- package/dist/models/model-order.d.ts.map +1 -1
- package/dist/models/model-order.js +9 -0
- package/dist/models/model-order.js.map +1 -1
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/model-roles.d.ts +25 -18
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +23 -27
- package/dist/models/model-roles.js.map +1 -1
- package/dist/models/model-setups.d.ts +46 -0
- package/dist/models/model-setups.d.ts.map +1 -0
- package/dist/models/model-setups.js +86 -0
- package/dist/models/model-setups.js.map +1 -0
- package/dist/models/plan-pools.d.ts +3 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +18 -6
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/models/provider-specs.d.ts +20 -0
- package/dist/models/provider-specs.d.ts.map +1 -1
- package/dist/models/provider-specs.js +13 -1
- package/dist/models/provider-specs.js.map +1 -1
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +26 -3
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/credential-material.d.ts +6 -0
- package/dist/policy/credential-material.d.ts.map +1 -1
- package/dist/policy/credential-material.js +37 -15
- package/dist/policy/credential-material.js.map +1 -1
- package/dist/policy/search-globs.d.ts +3 -0
- package/dist/policy/search-globs.d.ts.map +1 -1
- package/dist/policy/search-globs.js +118 -0
- package/dist/policy/search-globs.js.map +1 -1
- package/dist/policy/secret-hosts.d.ts +1 -0
- package/dist/policy/secret-hosts.d.ts.map +1 -1
- package/dist/policy/secret-hosts.js +1 -0
- package/dist/policy/secret-hosts.js.map +1 -1
- package/dist/policy/turned-away.d.ts.map +1 -1
- package/dist/policy/turned-away.js +1 -0
- package/dist/policy/turned-away.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +9 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +5 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/pairing-redeem.d.ts +23 -0
- package/dist/protocol/pairing-redeem.d.ts.map +1 -0
- package/dist/protocol/pairing-redeem.js +44 -0
- package/dist/protocol/pairing-redeem.js.map +1 -0
- package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
- package/dist/protocol/peer-mcp-server.js +1 -1
- package/dist/protocol/peer-mcp-server.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +16 -0
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +2 -0
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +8 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +2 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/protocol/sse.d.ts +20 -1
- package/dist/protocol/sse.d.ts.map +1 -1
- package/dist/protocol/sse.js +112 -17
- package/dist/protocol/sse.js.map +1 -1
- package/dist/schemas/agent.d.ts +32 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +12 -1
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/automations.d.ts +6 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +3 -1
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/devices.d.ts +17 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +2 -1
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/engines.d.ts +8 -1
- package/dist/schemas/engines.d.ts.map +1 -1
- package/dist/schemas/engines.js +1 -1
- package/dist/schemas/engines.js.map +1 -1
- package/dist/schemas/git/git-scope.d.ts +20 -0
- package/dist/schemas/git/git-scope.d.ts.map +1 -0
- package/dist/schemas/git/git-scope.js +37 -0
- package/dist/schemas/git/git-scope.js.map +1 -0
- package/dist/schemas/git/git.d.ts +1 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +9 -1
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +42 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +3 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/keep-warm.d.ts +1 -0
- package/dist/schemas/keep-warm.d.ts.map +1 -1
- package/dist/schemas/keep-warm.js +1 -1
- package/dist/schemas/keep-warm.js.map +1 -1
- package/dist/schemas/logs.d.ts +21 -0
- package/dist/schemas/logs.d.ts.map +1 -1
- package/dist/schemas/logs.js +19 -0
- package/dist/schemas/logs.js.map +1 -1
- package/dist/schemas/metrics.d.ts +12 -0
- package/dist/schemas/metrics.d.ts.map +1 -1
- package/dist/schemas/metrics.js +26 -8
- package/dist/schemas/metrics.js.map +1 -1
- package/dist/schemas/needs.d.ts +19 -0
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/needs.js +17 -3
- package/dist/schemas/needs.js.map +1 -1
- package/dist/schemas/offload.d.ts +154 -0
- package/dist/schemas/offload.d.ts.map +1 -1
- package/dist/schemas/offload.js +34 -0
- package/dist/schemas/offload.js.map +1 -1
- package/dist/schemas/privacy.d.ts +19 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +17 -2
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/project-bring-back.d.ts +172 -0
- package/dist/schemas/project-bring-back.d.ts.map +1 -0
- package/dist/schemas/project-bring-back.js +39 -0
- package/dist/schemas/project-bring-back.js.map +1 -0
- package/dist/schemas/providers/handoff.d.ts +77 -0
- package/dist/schemas/providers/handoff.d.ts.map +1 -0
- package/dist/schemas/providers/handoff.js +85 -0
- package/dist/schemas/providers/handoff.js.map +1 -0
- package/dist/schemas/providers/plan-limits.d.ts +27 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +8 -1
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +4 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/usage.d.ts +3 -1
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +7 -2
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/run-targets.d.ts.map +1 -1
- package/dist/schemas/run-targets.js +5 -2
- package/dist/schemas/run-targets.js.map +1 -1
- package/dist/schemas/settings-history.d.ts +1 -1
- package/dist/schemas/settings-history.d.ts.map +1 -1
- package/dist/schemas/settings-history.js +21 -0
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +67 -100
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +41 -24
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/short-codes.d.ts +31 -0
- package/dist/schemas/short-codes.d.ts.map +1 -0
- package/dist/schemas/short-codes.js +28 -0
- package/dist/schemas/short-codes.js.map +1 -0
- package/dist/schemas/system.d.ts +130 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +37 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/state/definition.d.ts +64 -40
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +1 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/versions.d.ts +6 -0
- package/dist/state/versions.d.ts.map +1 -1
- package/dist/state/versions.js +47 -0
- package/dist/state/versions.js.map +1 -1
- package/dist/text/embed.d.ts +2 -1
- package/dist/text/embed.d.ts.map +1 -1
- package/dist/text/embed.js +10 -5
- package/dist/text/embed.js.map +1 -1
- package/dist/text/title.d.ts.map +1 -1
- package/dist/text/title.js +4 -7
- package/dist/text/title.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +2 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -6
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +28 -6
- package/src/chores/probes.ts +3 -1
- package/src/contracts/agent.contract.ts +24 -3
- package/src/contracts/offload.contract.ts +21 -1
- package/src/contracts/privacy.contract.ts +13 -0
- package/src/contracts/settings.contract.ts +23 -0
- package/src/events/agent-events.ts +13 -0
- package/src/events/agent-words.ts +2 -3
- package/src/events/resume.test.ts +14 -2
- package/src/events/resume.ts +32 -3
- package/src/events/sandbox-notice.ts +31 -1
- package/src/events/transcript.ts +27 -2
- package/src/index.ts +5 -0
- package/src/models/agent-catalog.test.ts +4 -3
- package/src/models/agent-catalog.ts +10 -7
- package/src/models/agent-runtimes.ts +30 -1
- package/src/models/capability-ledger.test.ts +4 -0
- package/src/models/model-order.ts +13 -0
- package/src/models/model-pins.ts +5 -4
- package/src/models/model-roles.test.ts +9 -31
- package/src/models/model-roles.ts +35 -50
- package/src/models/model-setups.test.ts +100 -0
- package/src/models/model-setups.ts +151 -0
- package/src/models/plan-pools.test.ts +58 -0
- package/src/models/plan-pools.ts +38 -7
- package/src/models/provider-specs.ts +20 -2
- package/src/policy/command-classes.ts +34 -3
- package/src/policy/credential-material.ts +58 -16
- package/src/policy/search-globs.ts +135 -0
- package/src/policy/secret-hosts.ts +7 -0
- package/src/policy/tests/command-classes.test.ts +35 -0
- package/src/policy/tests/credential-material.test.ts +20 -0
- package/src/policy/tests/search-globs.test.ts +46 -1
- package/src/policy/tests/secret-hosts.test.ts +17 -1
- package/src/policy/turned-away.ts +2 -0
- package/src/protocol/doors/runner-protocol.ts +14 -1
- package/src/protocol/pairing-redeem.test.ts +86 -0
- package/src/protocol/pairing-redeem.ts +79 -0
- package/src/protocol/peer-mcp-server.ts +1 -2
- package/src/protocol/raw/raw-json-routes.ts +3 -0
- package/src/protocol/raw/raw-routes.ts +5 -0
- package/src/protocol/sse.test.ts +50 -1
- package/src/protocol/sse.ts +172 -26
- package/src/schemas/agent.ts +23 -2
- package/src/schemas/automations.ts +6 -2
- package/src/schemas/daemon-health.test.ts +28 -0
- package/src/schemas/devices.ts +11 -2
- package/src/schemas/engines.ts +1 -1
- package/src/schemas/git/git-scope.test.ts +91 -0
- package/src/schemas/git/git-scope.ts +71 -0
- package/src/schemas/git/git.ts +15 -1
- package/src/schemas/hosts.ts +7 -0
- package/src/schemas/keep-warm.ts +2 -1
- package/src/schemas/logs.ts +25 -0
- package/src/schemas/metrics.test.ts +80 -0
- package/src/schemas/metrics.ts +31 -9
- package/src/schemas/needs.ts +29 -3
- package/src/schemas/offload.ts +76 -0
- package/src/schemas/privacy.ts +27 -3
- package/src/schemas/project-bring-back.test.ts +15 -0
- package/src/schemas/project-bring-back.ts +78 -0
- package/src/schemas/providers/handoff.test.ts +42 -0
- package/src/schemas/providers/handoff.ts +128 -0
- package/src/schemas/providers/plan-limits.ts +14 -1
- package/src/schemas/providers/usage.ts +13 -7
- package/src/schemas/run-targets.ts +5 -2
- package/src/schemas/settings-history.ts +28 -0
- package/src/schemas/settings.test.ts +74 -21
- package/src/schemas/settings.ts +74 -44
- package/src/schemas/short-codes.ts +48 -0
- package/src/schemas/system.ts +63 -1
- package/src/state/history-state.ts +4 -0
- package/src/state/runtime-state.test.ts +4 -0
- package/src/state/runtime-state.ts +4 -0
- package/src/state/versions.test.ts +30 -1
- package/src/state/versions.ts +68 -0
- package/src/text/embed.test.ts +14 -0
- package/src/text/embed.ts +19 -10
- package/src/text/title.ts +5 -9
- package/src/text/transcript-fold.test.ts +61 -0
- package/src/text/transcript-fold.ts +31 -6
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { convertDocument } from "../documents/conversions.js";
|
|
2
|
+
import type { ModelRole } from "../models/model-roles.js";
|
|
3
|
+
import { rolePins } from "../models/model-setups.js";
|
|
2
4
|
import { SETTINGS_HISTORY } from "./settings-history.js";
|
|
3
5
|
import { RepoChecksFileSchema, RuleSchema, SandboxSettingsSchema, SandboxSettingsWriteSchema } from "./settings.js";
|
|
4
6
|
|
|
@@ -112,6 +114,20 @@ describe(`the folded keep-warm settings`, () => {
|
|
|
112
114
|
});
|
|
113
115
|
});
|
|
114
116
|
|
|
117
|
+
// Tool-result clearing lost its A/B and was removed (2026-10-12); a file that still switches it on reads, without it.
|
|
118
|
+
test(`a stored tool-result clearing switch is dropped, and the rest of the file kept`, () => {
|
|
119
|
+
const converted = convertDocument(SETTINGS_HISTORY, `object`, { iqSearch: true, toolResultClearing: true, toolResultClearingHoldout: 0.5 }, true);
|
|
120
|
+
expect(converted.value).toEqual({ iqSearch: true });
|
|
121
|
+
expect(SandboxSettingsSchema.parse(converted.value).iqSearch).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// What a job runs on, read the way every reader reads it, over a sandbox with nothing connected: the setups a stored
|
|
125
|
+
// file converts into are judged by what they make each job run on, not by their shape.
|
|
126
|
+
const runsOn = (stored: unknown, role: ModelRole): readonly unknown[] => {
|
|
127
|
+
const converted = convertDocument(SETTINGS_HISTORY, `object`, stored, true);
|
|
128
|
+
return rolePins(SandboxSettingsSchema.parse(converted.value), role, []);
|
|
129
|
+
};
|
|
130
|
+
|
|
115
131
|
// The pre-push fix's model list went with the fix a refused push used to offer (2026-09-28). A settings file that still
|
|
116
132
|
// names it must read, since an unknown role refuses the whole file, and every other job's list stays as its owner set it.
|
|
117
133
|
describe(`the retired pre-push fix role`, () => {
|
|
@@ -119,14 +135,8 @@ describe(`the retired pre-push fix role`, () => {
|
|
|
119
135
|
|
|
120
136
|
test(`is dropped from a stored file, leaving every other job's list, and the file reads`, () => {
|
|
121
137
|
const stored = { modelRoles: { "pipeline-fix": [pin], "pre-push-fix": [pin] } };
|
|
122
|
-
expect(SandboxSettingsSchema.safeParse(
|
|
123
|
-
|
|
124
|
-
expect(converted.value).toEqual({ modelRoles: { "pipeline-fix": [pin] } });
|
|
125
|
-
expect(SandboxSettingsSchema.parse(converted.value).modelRoles).toEqual({ "pipeline-fix": [pin] });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test(`leaves a file without it alone`, () => {
|
|
129
|
-
expect(convertDocument(SETTINGS_HISTORY, `object`, { modelRoles: { "pipeline-fix": [pin] } }, true).changes).toEqual([]);
|
|
138
|
+
expect(SandboxSettingsSchema.safeParse({ roleSetups: { "pre-push-fix": `high` } }).success).toBe(false);
|
|
139
|
+
expect(runsOn(stored, `pipeline-fix`)).toEqual([pin]);
|
|
130
140
|
});
|
|
131
141
|
});
|
|
132
142
|
|
|
@@ -138,19 +148,62 @@ describe(`the new Refactors job`, () => {
|
|
|
138
148
|
|
|
139
149
|
test(`takes the Maintenance chores list in a file from before it, and every other list stays`, () => {
|
|
140
150
|
const stored = { modelRoles: { "maintenance-chore": [opus, sol], "pipeline-fix": [sol] } };
|
|
141
|
-
|
|
142
|
-
expect(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
expect(runsOn(stored, `refactor-run`)).toEqual([opus, sol]);
|
|
152
|
+
expect(runsOn(stored, `pipeline-fix`)).toEqual([sol]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test(`leaves a list the owner set, even an emptied one`, () => {
|
|
156
|
+
expect(runsOn({ modelRoles: { "maintenance-chore": [opus], "refactor-run": [sol] } }, `refactor-run`)).toEqual([sol]);
|
|
157
|
+
expect(runsOn({ modelRoles: { "maintenance-chore": [opus], "refactor-run": [] } }, `refactor-run`)).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Setups (2026-10-12): per-job lists and the default list become named setups that jobs share.
|
|
162
|
+
describe(`per-job model lists`, () => {
|
|
163
|
+
const opus = { provider: `claude`, model: `claude-opus-5-5`, effort: `high` };
|
|
164
|
+
const haiku = { provider: `claude`, model: `claude-haiku-4-5` };
|
|
165
|
+
const sol = { provider: `codex`, model: `gpt-sol-6.1` };
|
|
166
|
+
|
|
167
|
+
test(`become setups that every job runs exactly as before on`, () => {
|
|
168
|
+
const cases = [
|
|
169
|
+
[`commit-message`, [haiku]],
|
|
170
|
+
[`session-title`, [haiku]],
|
|
171
|
+
[`pipeline-fix`, [opus, sol]],
|
|
172
|
+
[`refactor-run`, [opus, sol]],
|
|
173
|
+
[`handoff-summary`, [sol, haiku]],
|
|
174
|
+
[`safety-judge`, []],
|
|
175
|
+
] as const satisfies readonly (readonly [ModelRole, readonly unknown[]])[];
|
|
176
|
+
for (const [role, pins] of cases) {
|
|
177
|
+
expect(runsOn({ modelRoles: Object.fromEntries(cases) }, role)).toEqual(pins);
|
|
154
178
|
}
|
|
155
179
|
});
|
|
180
|
+
|
|
181
|
+
test(`share a setup where they held one list, which becomes the preset most of them suit`, () => {
|
|
182
|
+
const converted = convertDocument(SETTINGS_HISTORY, `object`, { modelRoles: { "commit-message": [haiku], "session-title": [haiku], "pipeline-fix": [opus], "handoff-summary": [sol] } }, true);
|
|
183
|
+
expect(converted.value).toEqual({
|
|
184
|
+
modelSetups: [
|
|
185
|
+
{ id: `low`, pins: [haiku] },
|
|
186
|
+
{ id: `setup-1`, name: `Setup 1`, pins: [sol] },
|
|
187
|
+
{ id: `high`, pins: [opus] },
|
|
188
|
+
],
|
|
189
|
+
roleSetups: { "commit-message": `low`, "session-title": `low`, "handoff-summary": `setup-1`, "pipeline-fix": `high` },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test(`carry the default list to every job that had none of its own`, () => {
|
|
194
|
+
const stored = { modelDefault: [sol], modelRoles: { "pipeline-fix": [opus] } };
|
|
195
|
+
expect(runsOn(stored, `commit-message`)).toEqual([sol]);
|
|
196
|
+
expect(runsOn(stored, `loop-iteration`)).toEqual([sol]);
|
|
197
|
+
expect(runsOn(stored, `pipeline-fix`)).toEqual([opus]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test(`leave a job that never had a list on none, running exactly as before`, () => {
|
|
201
|
+
const converted = convertDocument(SETTINGS_HISTORY, `object`, { modelRoles: {} }, true);
|
|
202
|
+
expect(converted.value).toEqual({ modelSetups: [], roleSetups: {} });
|
|
203
|
+
expect(runsOn({ modelRoles: {} }, `safety-judge`)).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test(`read once, and a file already in setups is left alone`, () => {
|
|
207
|
+
expect(convertDocument(SETTINGS_HISTORY, `object`, { modelSetups: [{ id: `low`, pins: [haiku] }], roleSetups: {} }, true).changes).toEqual([]);
|
|
208
|
+
});
|
|
156
209
|
});
|
package/src/schemas/settings.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { STATE_DIR } from "@intentic/constants";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { CommandJudgeModeSchema, ProjectInstallModeSchema } from "../policy/safety-policy.js";
|
|
6
6
|
import { ModelRoleSchema } from "../models/model-roles.js";
|
|
7
|
-
import {
|
|
7
|
+
import { MODEL_SETUP_ID, ModelSetupSchema } from "../models/model-setups.js";
|
|
8
|
+
import { AdmissionPolicySchema, AdmissionRuleSchema } from "./agent.js";
|
|
9
|
+
import { LimitHandoffSchema } from "./providers/handoff.js";
|
|
10
|
+
import { SessionsPlacementSchema } from "./offload.js";
|
|
8
11
|
import { LimitPolicySchema, RetryPolicySchema, RoomPolicySchema } from "./turn-break.js";
|
|
9
12
|
import { ZoneSchema } from "../time/zone.js";
|
|
10
13
|
// Which prompt base the agent runs before this turn composes anything on top: Intentic's own (default), Claude Code's
|
|
@@ -316,23 +319,6 @@ export const SandboxSettingsSchema = z.object({
|
|
|
316
319
|
.describe(
|
|
317
320
|
"Your own instructions, used only when the mode above says custom. Then it is the whole of them: both built-in bases go, and so does everything this product would otherwise add, including the guidance the chat's own cards are driven by. That is the price of total control.",
|
|
318
321
|
),
|
|
319
|
-
// Measured by the `clearing` experiment: the gateway in front of a native Claude turn replaces old tool results with
|
|
320
|
-
// a placeholder, a chunk at a time, so the cached prompt breaks once per chunk rather than on every call
|
|
321
|
-
// (privacy/gateway/tool-result-clearing.ts). Off by default until that experiment says it pays.
|
|
322
|
-
toolResultClearing: z
|
|
323
|
-
.boolean()
|
|
324
|
-
.default(false)
|
|
325
|
-
.describe(
|
|
326
|
-
"Replace old tool results from a Claude conversation once it grows long, a large chunk at a time, keeping the most recent ones whole. The agent sees a short placeholder where each one was and can run the tool again. Smaller prompts cost less and answer sooner, at the risk of the agent re-reading what it already saw.",
|
|
327
|
-
),
|
|
328
|
-
toolResultClearingHoldout: z
|
|
329
|
-
.number()
|
|
330
|
-
.min(0)
|
|
331
|
-
.max(1)
|
|
332
|
-
.default(0.5)
|
|
333
|
-
.describe(
|
|
334
|
-
"What share of conversations keep every tool result, so the two can be compared. Whole conversations rather than individual turns, because what was dropped stays dropped for the rest of the session.",
|
|
335
|
-
),
|
|
336
322
|
iqSearch: z
|
|
337
323
|
.boolean()
|
|
338
324
|
.default(false)
|
|
@@ -351,18 +337,19 @@ export const SandboxSettingsSchema = z.object({
|
|
|
351
337
|
.boolean()
|
|
352
338
|
.default(false)
|
|
353
339
|
.describe(
|
|
354
|
-
"Open every conversation with a map of the project it starts in:
|
|
340
|
+
"Open every conversation with a map of the project it starts in: what is in it, what each part is for, and where the agent is standing. Worked out fresh each time rather than written down anywhere, because a written layout is wrong within a fortnight. Off by default, since it spends tokens on the first message of every conversation.",
|
|
355
341
|
),
|
|
356
342
|
// Judged on the opening turn's own directory listings, not averaged across the conversation, or a twelve-turn
|
|
357
|
-
// conversation would divide the effect by twelve.
|
|
358
|
-
//
|
|
343
|
+
// conversation would divide the effect by twelve. From 2026-10-06 to 10-12 the held-out share got the full map
|
|
344
|
+
// against a compact one; that changed nothing measurable, so the compact form went and the holdout opens without a
|
|
345
|
+
// map again.
|
|
359
346
|
workspaceMapHoldout: z
|
|
360
347
|
.number()
|
|
361
348
|
.min(0)
|
|
362
349
|
.max(1)
|
|
363
350
|
.default(0)
|
|
364
351
|
.describe(
|
|
365
|
-
"What share of conversations to open
|
|
352
|
+
"What share of conversations to open without the map, so the two can be compared. Whole conversations rather than individual turns, because the map is sent once and stays in the conversation's history afterwards.",
|
|
366
353
|
),
|
|
367
354
|
// The one composed piece that is WRITTEN rather than derived: a monthly automation rewrites it off the session
|
|
368
355
|
// corpus, so it carries what no scan of the tree can (which commands really work here, what the box can take, how
|
|
@@ -402,15 +389,22 @@ export const SandboxSettingsSchema = z.object({
|
|
|
402
389
|
.max(1)
|
|
403
390
|
.default(0)
|
|
404
391
|
.describe("What share of commands to leave untrimmed, so the saving can be measured against a real comparison rather than estimated."),
|
|
405
|
-
//
|
|
406
|
-
//
|
|
392
|
+
// The model lists jobs run on, edited once for every job put there (models/model-setups.ts). The three presets need
|
|
393
|
+
// no entry: absent, each is worked out from what is connected, so a sandbox runs before anyone opens this page.
|
|
394
|
+
modelSetups: z
|
|
395
|
+
.array(ModelSetupSchema)
|
|
396
|
+
.max(20)
|
|
397
|
+
.default([])
|
|
398
|
+
.describe(
|
|
399
|
+
"The model setups jobs run on: Low, Medium and High, which every sandbox has, and any you make. Each is an ordered list of models, tried in order so one spent account does not take its jobs down. A preset left out of this list, or listed without models, is worked out from the accounts you have connected. A setup runs only the jobs put on it (`roleSetups`).",
|
|
400
|
+
),
|
|
407
401
|
// `partialRecord`, not `record`: an exhaustive one would force every settings file to list every role, and adding
|
|
408
|
-
// one would invalidate them all. Absent IS "no
|
|
409
|
-
|
|
410
|
-
.partialRecord(ModelRoleSchema, z.
|
|
402
|
+
// one would invalidate them all. Absent IS "on no setup": nothing runs a job nobody placed.
|
|
403
|
+
roleSetups: z
|
|
404
|
+
.partialRecord(ModelRoleSchema, z.string().regex(MODEL_SETUP_ID))
|
|
411
405
|
.default({})
|
|
412
406
|
.describe(
|
|
413
|
-
"Which
|
|
407
|
+
"Which setup each job runs on: commit messages, session titles, the safety judge, pipeline fixes and every other place this sandbox picks a model for you. Nothing is chosen for you: a job left out, or on a setup with no models, has none, so a one-shot job does not run and a whole session opens on whatever your own chat is set to.",
|
|
414
408
|
),
|
|
415
409
|
// Folded into the router's prompt (agent/prompt/chat-router.ts), never into a turn's own. Capped small on
|
|
416
410
|
// purpose: the reading runs under a 5s deadline, and this text is paid for on every chat that opens on Auto.
|
|
@@ -457,13 +451,28 @@ export const SandboxSettingsSchema = z.object({
|
|
|
457
451
|
memoryPolicy: RoomPolicySchema.default("resend").describe(
|
|
458
452
|
"What happens to a message the sandbox held because it was short of memory: nothing ran, so nothing was spent. `resend` sends it by itself once memory has stayed free for a while, one held conversation at a time. `wait` holds it until somebody presses Send anyway. The sandbox-wide default; any one conversation can say otherwise.",
|
|
459
453
|
),
|
|
454
|
+
// How a held turn continues once its cache has gone cold (schemas/providers/handoff.ts). The card asks each time and
|
|
455
|
+
// preselects this; a booked send (a move, a resend at the reset) uses it unless a person picked on the card.
|
|
456
|
+
limitHandoff: LimitHandoffSchema.default("suggested").describe(
|
|
457
|
+
"How a turn a spent usage limit held continues, on this account at the reset or on another: `carry` resumes the session as it is (the model keeps everything and re-reads all of it), `trim` resumes a copy with older tool output cleared (every message and tool call stays), `summary` opens a fresh session with a smaller model's summary and the last exchanges word for word. `suggested` picks by the conversation's size, using the two thresholds beside this. The chat's card preselects it and lets you pick another for that turn.",
|
|
458
|
+
),
|
|
459
|
+
// Read by the `suggested` hand-off only. Kept under its old name: before 2026-10-10 it chose between carrying and a
|
|
460
|
+
// short record hand-off for a move, and a saved value still means "carry under this".
|
|
460
461
|
limitMoveCarryUnder: z
|
|
461
462
|
.number()
|
|
462
463
|
.int()
|
|
463
464
|
.min(0)
|
|
464
|
-
.default(
|
|
465
|
+
.default(150_000)
|
|
465
466
|
.describe(
|
|
466
|
-
"
|
|
467
|
+
"For the suggested hand-off: carry the session as it is while the conversation's context is under this many tokens; at or above it, trim it (or summarise it, past the next threshold). Zero never suggests carrying.",
|
|
468
|
+
),
|
|
469
|
+
handoffSummaryOver: z
|
|
470
|
+
.number()
|
|
471
|
+
.int()
|
|
472
|
+
.min(0)
|
|
473
|
+
.default(700_000)
|
|
474
|
+
.describe(
|
|
475
|
+
"For the suggested hand-off: at or above this many tokens of context, suggest a summary rather than a trimmed session, where a model is set for hand-off summaries.",
|
|
467
476
|
),
|
|
468
477
|
// Off by default: every refresh spends the account's own allowance on a conversation nobody is using yet. Before
|
|
469
478
|
// 2026-09-26 these were four top-level settings (settings-history.ts folds them).
|
|
@@ -493,13 +502,17 @@ export const SandboxSettingsSchema = z.object({
|
|
|
493
502
|
// the heavy-command rule an agent's command matched (workload/heavy-commands.ts), each naming a runner id. A
|
|
494
503
|
// runner that is offline, outdated or full hands the work back here, which the command's output says. Never pushed
|
|
495
504
|
// to a runner itself (definition/definition.ts), which must not pass work on again.
|
|
505
|
+
// `sessions`: what a new conversation is told when this machine is busy and a runner has room
|
|
506
|
+
// (runners/fleet-advice.ts). Suggesting is the default: it costs one line above the composer, and moving a session
|
|
507
|
+
// without being asked is the owner's choice to make.
|
|
496
508
|
offload: z
|
|
497
509
|
.object({
|
|
498
510
|
commands: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
511
|
+
sessions: SessionsPlacementSchema.default("suggest"),
|
|
499
512
|
})
|
|
500
|
-
.default({ commands: {} })
|
|
513
|
+
.default({ commands: {}, sessions: "suggest" })
|
|
501
514
|
.describe(
|
|
502
|
-
"Which
|
|
515
|
+
"Which work runs on a runner on one of your machines instead of this sandbox. `commands`: agents' heavy commands by the kind the heavy-command rules sort them into (tests, typechecks, verify…); the code travels as it stands, uncommitted work included, the output streams back, and any file the command changed comes back with it, while a machine that is offline, outdated or busy hands the work back to this sandbox, and the output says so. `sessions`: what happens to a new conversation while this machine is busy and one of your runners has room: `suggest` offers that machine above the composer, `auto` starts it there unless you keep it here, `off` says nothing.",
|
|
503
516
|
),
|
|
504
517
|
// A person answering a need is exactly the moment they want the work to go on (docs/architecture/needs.md), unlike a
|
|
505
518
|
// restart nobody watched, so this one is on to begin with.
|
|
@@ -517,13 +530,15 @@ export const SandboxSettingsSchema = z.object({
|
|
|
517
530
|
),
|
|
518
531
|
// Which repositories' own declarations (`<repo>/.intentic/checks.json`) the owner has switched on, each against the
|
|
519
532
|
// fingerprint of what was declared when they did. A declaration that has since changed no longer matches its
|
|
520
|
-
// fingerprint
|
|
521
|
-
//
|
|
533
|
+
// fingerprint: the daemon finds the version that did in the repository's history, keeps running each check that is
|
|
534
|
+
// still word for word one of its checks, and holds every new or rewritten one, which is what makes adoption a
|
|
535
|
+
// decision about a command rather than a permanent permission on a folder, without one added check switching off
|
|
536
|
+
// all the others. Keyed by repo id, so `/` in the key is ordinary.
|
|
522
537
|
adoptedChecks: z
|
|
523
538
|
.record(z.string(), z.string())
|
|
524
539
|
.default({})
|
|
525
540
|
.describe(
|
|
526
|
-
"Which repositories may run the checks they declare for themselves, and exactly which version of those checks you agreed to. A repository's declaration does nothing until it appears here, the same rule git keeps for hooks, which are never cloned; and a
|
|
541
|
+
"Which repositories may run the checks they declare for themselves, and exactly which version of those checks you agreed to. A repository's declaration does nothing until it appears here, the same rule git keeps for hooks, which are never cloned; and a check it adds or rewrites afterwards waits until you look at it again, while the ones you agreed to keep running.",
|
|
527
542
|
),
|
|
528
543
|
// The owner's alone, since a rule can hold work. A command is refused here: it belongs to the repository it checks,
|
|
529
544
|
// in that repository's `.intentic/checks.json`, never in settings.
|
|
@@ -558,7 +573,7 @@ export const SandboxSettingsSchema = z.object({
|
|
|
558
573
|
.default({})
|
|
559
574
|
.describe("What an agent may do out in the world, per kind of action: go ahead, ask first, or never."),
|
|
560
575
|
// Command policy itself lives at .intentic/config/safety.md (edited by the Safety page), not here; this only says
|
|
561
|
-
// whether the judge runs at all. Which model judges is the `safety-judge`
|
|
576
|
+
// whether the judge runs at all. Which model judges is the `safety-judge` job's setup (`roleSetups`), not a field here.
|
|
562
577
|
commandJudge: CommandJudgeModeSchema.default("on").describe(
|
|
563
578
|
"Whether a model reads your safety policy before a flagged command runs. Off judges nothing and asks about nothing; Watch judges everything and records it without ever interrupting you, which is how you find out what your policy actually does before you let it stop anything; On lets the verdict decide. Wiping a disk or deleting under /history asks at every setting — that rule is typed rather than judged, and cannot be turned off.",
|
|
564
579
|
),
|
|
@@ -608,6 +623,20 @@ export const AudienceStateSchema = z.object({
|
|
|
608
623
|
});
|
|
609
624
|
export type AudienceState = z.infer<typeof AudienceStateSchema>;
|
|
610
625
|
|
|
626
|
+
// What a person has said about the editor's getting-started checklist on this sandbox, kept per member like the
|
|
627
|
+
// audience so it reads the same on every device. Only what the sandbox's own data cannot answer: whether a step is DONE
|
|
628
|
+
// is read from that data each time (a workspace with files, an agent started, a model connected, work landed), so this
|
|
629
|
+
// holds nothing but the reader's own choices. Step names are the editor's, opaque here.
|
|
630
|
+
export const GettingStartedSchema = z.object({
|
|
631
|
+
hidden: z.boolean().optional().describe("Whether this person put the checklist away. Absent until they have."),
|
|
632
|
+
skipped: z
|
|
633
|
+
.array(z.string().min(1).max(40))
|
|
634
|
+
.max(20)
|
|
635
|
+
.optional()
|
|
636
|
+
.describe("The checklist steps this person passed over, by the editor's own step names."),
|
|
637
|
+
});
|
|
638
|
+
export type GettingStarted = z.infer<typeof GettingStartedSchema>;
|
|
639
|
+
|
|
611
640
|
// A browser telling the sandbox which clock IT is on. An offer, not an instruction: see `adoptTimezone`.
|
|
612
641
|
export const TimezoneOfferSchema = z.object({
|
|
613
642
|
timezone: ZoneSchema.describe("The zone the offering machine is in, as an IANA name like Europe/Warsaw."),
|
|
@@ -658,13 +687,9 @@ export const SavingsArmSchema = z.object({ turns: z.number(), mean: z.number() }
|
|
|
658
687
|
// openingListings: directory listings a turn ran to orient itself (the project map).
|
|
659
688
|
// callsBeforeTarget: how far a turn walked before touching a file it went on to edit.
|
|
660
689
|
// failedCalls: tool calls that ended in error (the field notes, whose largest section is a failure taxonomy).
|
|
661
|
-
//
|
|
662
|
-
// contextPerCall: prompt tokens per model call, cached or not (tool-result clearing, which exists to shrink it).
|
|
663
|
-
// Never cost: each mechanism moves one small part of a turn's work, inside the noise of the rest. Round trips are the
|
|
664
|
-
// nearest a reading comes to it, and still a count of calls, not of tokens or money; the prompt a call carries is the
|
|
665
|
-
// one exception, for the one mechanism that changes nothing else.
|
|
690
|
+
// Never cost: each mechanism moves one small part of a turn's work, inside the noise of the rest.
|
|
666
691
|
export const TurnMetricReadingSchema = z.object({
|
|
667
|
-
metric: z.enum(["searchCalls", "openingSearches", "openingListings", "callsBeforeTarget", "failedCalls"
|
|
692
|
+
metric: z.enum(["searchCalls", "openingSearches", "openingListings", "callsBeforeTarget", "failedCalls"]),
|
|
668
693
|
on: SavingsArmSchema,
|
|
669
694
|
off: SavingsArmSchema,
|
|
670
695
|
// Additional control turns to reach a fixed target resolution, not today's delta (which inherits noise and always
|
|
@@ -727,7 +752,6 @@ export const SavingsReportSchema = z.object({
|
|
|
727
752
|
// Same absence rule as `search`: not measured, never zero.
|
|
728
753
|
map: TurnExperimentSchema.optional(),
|
|
729
754
|
notes: TurnExperimentSchema.optional(),
|
|
730
|
-
clearing: TurnExperimentSchema.optional(),
|
|
731
755
|
});
|
|
732
756
|
export type SavingsReport = z.infer<typeof SavingsReportSchema>;
|
|
733
757
|
|
|
@@ -793,7 +817,13 @@ export const RepoChecksSummarySchema = z.object({
|
|
|
793
817
|
changed: z
|
|
794
818
|
.boolean()
|
|
795
819
|
.describe(
|
|
796
|
-
"Whether the declaration
|
|
820
|
+
"Whether the declaration gained or rewrote a check since it was adopted. What changed waits until the owner looks again; the checks the owner already agreed to, word for word, keep running (`held` says which is which). True only for a repository that was adopted before.",
|
|
821
|
+
),
|
|
822
|
+
held: z
|
|
823
|
+
.array(z.boolean())
|
|
824
|
+
.optional()
|
|
825
|
+
.describe(
|
|
826
|
+
"For a changed declaration, which of its checks wait for the owner, in the file's order: true for one that is new or rewritten since they agreed, false for one still running as agreed, or retired. Absent when nothing changed, and from a sandbox that holds a changed declaration whole.",
|
|
797
827
|
),
|
|
798
828
|
error: z.string().optional().describe("Why the file could not be read, when it exists but does not parse. The checks list is empty in that case."),
|
|
799
829
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// short codes: the single-use 8-character code a copy-paste command carries instead of a token. The sandbox mints one
|
|
2
|
+
// beside each device, sync or server-connect token it hands out; the install script redeems it at the sandbox's public
|
|
3
|
+
// POST /system/redeem/<code>, which answers `KEY=value` lines (KIND, SANDBOX_URL, then the kind's own values).
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { INVENTORY_NAME_MAX, INVENTORY_NAME_RE } from "./inventory.js";
|
|
6
|
+
|
|
7
|
+
// Crockford base32 (no I, L, O, U), 8 characters: 40 bits. Redemption accepts any case and one hyphen in the middle.
|
|
8
|
+
export const SHORT_CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
9
|
+
export const SHORT_CODE_LENGTH = 8;
|
|
10
|
+
export const ShortCodeSchema = z.string().regex(/^[0-9A-HJKMNP-TV-Z]{8}$/);
|
|
11
|
+
|
|
12
|
+
// What each kind of code redeems to, in its answer's KIND line.
|
|
13
|
+
export const ShortCodeKindSchema = z.enum(["device", "sync", "server"]);
|
|
14
|
+
export type ShortCodeKind = z.infer<typeof ShortCodeKindSchema>;
|
|
15
|
+
|
|
16
|
+
// A value that rides one `KEY=value` line: no line breaks or other control characters.
|
|
17
|
+
const lineValue = z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(1024)
|
|
21
|
+
.regex(/^[^\p{Cc}]+$/u);
|
|
22
|
+
|
|
23
|
+
// A device or sync pairing as minted: the token (unchanged) and, additively, the code that stands for it.
|
|
24
|
+
export const PairingAnswerSchema = z.object({
|
|
25
|
+
token: z.string(),
|
|
26
|
+
expiresIn: z.number().describe("Seconds until the token, and its code, expire."),
|
|
27
|
+
code: ShortCodeSchema.describe("Single-use short code a command carries instead of the token."),
|
|
28
|
+
});
|
|
29
|
+
export type PairingAnswer = z.infer<typeof PairingAnswerSchema>;
|
|
30
|
+
|
|
31
|
+
// POST /system/sync/pair's optional body: the person's chosen local folder and takeover, handed out with the code.
|
|
32
|
+
// It goes to their own sandbox, never the platform.
|
|
33
|
+
export const SyncPairRequestSchema = z.object({
|
|
34
|
+
syncDir: lineValue.optional().describe("Local folder to sync, as the person chose it (`~` expanded on the device)."),
|
|
35
|
+
takeover: z.boolean().optional().describe("Take sync over from another machine already enrolled."),
|
|
36
|
+
});
|
|
37
|
+
export type SyncPairRequest = z.infer<typeof SyncPairRequestSchema>;
|
|
38
|
+
|
|
39
|
+
// POST /system/enroll/code: mints a server-connect code backed by a single-use enroll token.
|
|
40
|
+
export const EnrollCodeRequestSchema = z.object({
|
|
41
|
+
hostName: z.string().min(1).max(INVENTORY_NAME_MAX).regex(INVENTORY_NAME_RE).optional().describe("What the server will be called."),
|
|
42
|
+
});
|
|
43
|
+
export type EnrollCodeRequest = z.infer<typeof EnrollCodeRequestSchema>;
|
|
44
|
+
export const EnrollCodeAnswerSchema = z.object({
|
|
45
|
+
code: ShortCodeSchema,
|
|
46
|
+
expiresIn: z.number().describe("Seconds until the code and its enroll token expire."),
|
|
47
|
+
});
|
|
48
|
+
export type EnrollCodeAnswer = z.infer<typeof EnrollCodeAnswerSchema>;
|
package/src/schemas/system.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { BootProgressSchema } from "../events/system-events.js";
|
|
3
|
+
import { AnnounceStateSchema } from "./platform-link.js";
|
|
4
|
+
import { StatePlanSchema, StateStatusSchema } from "./state-plan.js";
|
|
3
5
|
import { AutoUpdateSchema, UpdateOutcomeSchema, WithdrawnReleaseSchema } from "./updates.js";
|
|
4
6
|
// version is this build's baked-in version; latest/updateAvailable come from comparing it to the published stable
|
|
5
7
|
// release.
|
|
@@ -316,3 +318,63 @@ export interface AuthenticationOptionsJSON {
|
|
|
316
318
|
// pressing it can succeed.
|
|
317
319
|
readonly available: boolean;
|
|
318
320
|
}
|
|
321
|
+
|
|
322
|
+
// Whether this sandbox's public address answers, probed by the box itself; the same shape as `announce` beside it, so
|
|
323
|
+
// `ic sandbox doctor` and the setup wizard can name whichever link broke.
|
|
324
|
+
export const ReachStateSchema = z.object({
|
|
325
|
+
// off: nothing to probe; checking: no verdict yet; reachable: its own public address answered with our id.
|
|
326
|
+
state: z.enum(["off", "checking", "reachable", "unreachable"]),
|
|
327
|
+
// Why, for "unreachable", already in the user's terms since the wizard renders it verbatim.
|
|
328
|
+
detail: z.string().optional(),
|
|
329
|
+
// False once the give-up window is spent; only a restart retries after that.
|
|
330
|
+
retrying: z.boolean().optional(),
|
|
331
|
+
at: z.number().optional(),
|
|
332
|
+
});
|
|
333
|
+
export type ReachState = z.infer<typeof ReachStateSchema>;
|
|
334
|
+
|
|
335
|
+
// Which build answers, so "is the fix live?" is one read.
|
|
336
|
+
export const BuildStampSchema = z.object({
|
|
337
|
+
version: z.string(),
|
|
338
|
+
commit: z.string().optional(),
|
|
339
|
+
builtAt: z.string().optional(),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// GET /health: the "is a daemon there" probe every flow uses (launch scripts, /setup, the browser's loopback probe,
|
|
343
|
+
// the platform's hosted gate, the onboarding lane), served raw before any authentication. `ic` reads it too, in Rust
|
|
344
|
+
// (_sandbox/ic/src/health.rs), which cannot import this and reads only `boot`, `state` and `announce` loosely.
|
|
345
|
+
export const DaemonHealthSchema = z.object({
|
|
346
|
+
ok: z.literal(true),
|
|
347
|
+
// The sandbox id, so a loopback probe can confirm it reached this daemon and not an unrelated process. Absent on a
|
|
348
|
+
// daemon with no connect token.
|
|
349
|
+
sandboxId: z.string().optional(),
|
|
350
|
+
// Which posture answers (the daemon's platform/profile.ts); needed before any authenticated read.
|
|
351
|
+
profile: z.string(),
|
|
352
|
+
// Tells "not answering" from "still converging".
|
|
353
|
+
boot: BootProgressSchema,
|
|
354
|
+
// Whether an update's state conversions are still uncommitted; the host rolls the update back when they stay open.
|
|
355
|
+
state: StateStatusSchema,
|
|
356
|
+
// Whether this daemon reached the platform.
|
|
357
|
+
announce: AnnounceStateSchema,
|
|
358
|
+
reach: ReachStateSchema,
|
|
359
|
+
// How the world reaches here: a dialed tunnel, or loopback only.
|
|
360
|
+
reachedBy: z.enum(["tunnel", "loopback"]),
|
|
361
|
+
build: BuildStampSchema,
|
|
362
|
+
});
|
|
363
|
+
export type DaemonHealth = z.infer<typeof DaemonHealthSchema>;
|
|
364
|
+
|
|
365
|
+
// The same answer as a reader outside the daemon takes it: from a daemon of any age, so every field may be missing,
|
|
366
|
+
// and one of an unexpected shape reads as missing rather than failing the whole answer. A daemon older than the boot
|
|
367
|
+
// report said `ready` at the top level instead.
|
|
368
|
+
export const DaemonHealthReadingSchema = z.object({
|
|
369
|
+
ok: z.boolean().optional().catch(undefined),
|
|
370
|
+
ready: z.boolean().optional().catch(undefined),
|
|
371
|
+
sandboxId: z.string().optional().catch(undefined),
|
|
372
|
+
profile: z.string().optional().catch(undefined),
|
|
373
|
+
boot: z.object({ ready: z.boolean().optional().catch(undefined) }).optional().catch(undefined),
|
|
374
|
+
state: z.object({ journal: z.string().optional().catch(undefined) }).optional().catch(undefined),
|
|
375
|
+
announce: AnnounceStateSchema.optional().catch(undefined),
|
|
376
|
+
reach: ReachStateSchema.optional().catch(undefined),
|
|
377
|
+
reachedBy: z.string().optional().catch(undefined),
|
|
378
|
+
build: BuildStampSchema.optional().catch(undefined),
|
|
379
|
+
});
|
|
380
|
+
export type DaemonHealthReading = z.infer<typeof DaemonHealthReadingSchema>;
|
|
@@ -31,6 +31,10 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
31
31
|
// (settings/member-audience.ts). Carried: it is theirs, and asking again after a move would be a question already
|
|
32
32
|
// answered.
|
|
33
33
|
{ path: "member-audience.json", portability: "carry" },
|
|
34
|
+
// Each person's own answers to the editor's getting-started checklist (put away, a step passed over), keyed by sign-in
|
|
35
|
+
// address (settings/member-getting-started.ts). Carried for the same reason: a checklist someone put away should not
|
|
36
|
+
// come back after a move.
|
|
37
|
+
{ path: "member-getting-started.json", portability: "carry" },
|
|
34
38
|
{ path: "activity.jsonl", portability: "carry" },
|
|
35
39
|
{ path: "usage.jsonl", portability: "carry" },
|
|
36
40
|
{ path: "account-usage.json", portability: "carry" },
|
|
@@ -36,6 +36,10 @@ describe(`staleRuntimeQueryKeys`, () => {
|
|
|
36
36
|
// The feed is ["activity", <sandbox>] and the status ["activity-status", <sandbox>]: two keys, not one prefix.
|
|
37
37
|
expect(staleRuntimeQueryKeys([`activity`])).toEqual([[`activity`], [`activity-status`]]);
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
it(`re-reads the Allowed list when a card's yes is written or taken back, which no file push carries`, () => {
|
|
41
|
+
expect(staleRuntimeQueryKeys([`grants`])).toEqual([[`grants`]]);
|
|
42
|
+
});
|
|
39
43
|
});
|
|
40
44
|
|
|
41
45
|
describe(`RUNTIME_DOMAIN_BINDINGS`, () => {
|
|
@@ -66,6 +66,10 @@ const RUNTIME_DOMAINS = [
|
|
|
66
66
|
// An automatic update started waiting, changed what it waits on, began counting down or handed itself to the
|
|
67
67
|
// machine: what every page needs to hear before the sandbox goes quiet under it, wherever the reader is.
|
|
68
68
|
{ domain: "update", invalidates: [["update"]] },
|
|
69
|
+
|
|
70
|
+
// A standing yes written or taken back outside a need's answer: a permission card's pass, a release, a purge. They
|
|
71
|
+
// live under the auth root, which no file push watches, and the Allowed list must move with the card that wrote one.
|
|
72
|
+
{ domain: "grants", invalidates: [["grants"]] },
|
|
69
73
|
] as const satisfies readonly RuntimeDomainBinding[];
|
|
70
74
|
|
|
71
75
|
export const RUNTIME_DOMAIN_BINDINGS: readonly RuntimeDomainBinding[] = RUNTIME_DOMAINS;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEV_VERSION, isBehind, isNewer } from "./versions.js";
|
|
1
|
+
import { compareVersions, DEV_VERSION, isBehind, isNewer, parseVersion } from "./versions.js";
|
|
2
2
|
|
|
3
3
|
test("isNewer compares dotted numeric versions", () => {
|
|
4
4
|
expect(isNewer("1.3.0", "1.2.9")).toBe(true); // newer minor
|
|
@@ -34,3 +34,32 @@ test("a version that isn't dotted-numeric can only ever withhold the nudge", ()
|
|
|
34
34
|
test("isBehind flags the hand-written version agents used to carry", () => {
|
|
35
35
|
expect(isBehind("0.1.0", "1.183.0")).toBe(true);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
test("compareVersions puts a release ahead of its own prereleases", () => {
|
|
39
|
+
expect(compareVersions("1.2.0", "1.2.0-rc.1")).toBe(1);
|
|
40
|
+
expect(compareVersions("1.2.0-rc.1", "1.2.0")).toBe(-1);
|
|
41
|
+
expect(compareVersions("1.2.0-rc.2", "1.2.0-rc.1")).toBe(1);
|
|
42
|
+
expect(compareVersions("1.2.0-rc.10", "1.2.0-rc.9")).toBe(1); // numeric identifiers numerically
|
|
43
|
+
expect(compareVersions("1.2.0-rc.1.1", "1.2.0-rc.1")).toBe(1); // more identifiers, equal so far, is later
|
|
44
|
+
expect(compareVersions("1.2.0-1", "1.2.0-alpha")).toBe(-1); // a number sorts below a word
|
|
45
|
+
expect(compareVersions("1.1.9", "1.2.0-rc.1")).toBe(-1); // the core decides before the prerelease
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("compareVersions tolerates a leading v and ignores build metadata", () => {
|
|
49
|
+
expect(compareVersions("v1.10.0", "1.9.0")).toBe(1);
|
|
50
|
+
expect(compareVersions("V2.0.0", "v2.0.0")).toBe(0);
|
|
51
|
+
expect(compareVersions("1.2.0+build.5", "1.2.0")).toBe(0);
|
|
52
|
+
expect(compareVersions("1.2", "1.2.0")).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("compareVersions sorts anything unparseable below every version", () => {
|
|
56
|
+
expect(compareVersions("unknown", "0.0.1")).toBe(-1);
|
|
57
|
+
expect(compareVersions("1.0.0", "")).toBe(1);
|
|
58
|
+
expect(compareVersions("latest", "unknown")).toBe(0);
|
|
59
|
+
expect(["1.2.0-rc.1", "unknown", "1.10.0", "v1.2.0"].sort((a, b) => compareVersions(b, a))).toEqual(["1.10.0", "v1.2.0", "1.2.0-rc.1", "unknown"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("parseVersion reads the parts and the prerelease", () => {
|
|
63
|
+
expect(parseVersion("v1.2.3-rc.1+abc")).toEqual({ parts: [1, 2, 3], prerelease: ["rc", "1"] });
|
|
64
|
+
expect(parseVersion("1.x")).toBe(undefined);
|
|
65
|
+
});
|
package/src/state/versions.ts
CHANGED
|
@@ -24,3 +24,71 @@ export const DEV_VERSION = `0.0.0`;
|
|
|
24
24
|
// A malformed version compares as not-newer only, so it can withhold a nag but never invent one.
|
|
25
25
|
export const isBehind = (installed: string | undefined, latest: string | undefined): boolean =>
|
|
26
26
|
installed !== undefined && latest !== undefined && installed !== DEV_VERSION && isNewer(latest, installed);
|
|
27
|
+
|
|
28
|
+
// A version read as semver: a leading `v` tolerated, any number of dotted numeric parts (a missing one is 0), a
|
|
29
|
+
// prerelease after `-`, and build metadata after `+`, which never orders anything.
|
|
30
|
+
export interface ParsedVersion {
|
|
31
|
+
readonly parts: readonly number[];
|
|
32
|
+
// The dot-separated prerelease identifiers; empty for a release.
|
|
33
|
+
readonly prerelease: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const VERSION = /^v?(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i;
|
|
37
|
+
|
|
38
|
+
// Undefined for anything that is not a version (`unknown`, `latest`, an empty string).
|
|
39
|
+
export const parseVersion = (version: string): ParsedVersion | undefined => {
|
|
40
|
+
const match = VERSION.exec(version.trim());
|
|
41
|
+
if (match === null) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return { parts: match[1]!.split(`.`).map(Number), prerelease: match[2] === undefined ? [] : match[2].split(`.`) };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Semver's rule for one identifier: numbers numerically and below words, words by ASCII.
|
|
48
|
+
const compareIdentifier = (left: string, right: string): number => {
|
|
49
|
+
const [leftNumeric, rightNumeric] = [/^\d+$/.test(left), /^\d+$/.test(right)];
|
|
50
|
+
if (leftNumeric && rightNumeric) {
|
|
51
|
+
return Number(left) - Number(right);
|
|
52
|
+
}
|
|
53
|
+
if (leftNumeric !== rightNumeric) {
|
|
54
|
+
return leftNumeric ? -1 : 1;
|
|
55
|
+
}
|
|
56
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const comparePrerelease = (left: readonly string[], right: readonly string[]): number => {
|
|
60
|
+
// A release outranks every prerelease of itself: 1.2.0 > 1.2.0-rc.1.
|
|
61
|
+
if (left.length === 0 || right.length === 0) {
|
|
62
|
+
return right.length - left.length;
|
|
63
|
+
}
|
|
64
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
65
|
+
const [l, r] = [left[index], right[index]];
|
|
66
|
+
if (l === undefined || r === undefined) {
|
|
67
|
+
// Equal so far, so the longer set of identifiers is the later one: rc.1.1 > rc.1.
|
|
68
|
+
return l === undefined ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
const difference = compareIdentifier(l, r);
|
|
71
|
+
if (difference !== 0) {
|
|
72
|
+
return difference;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Negative when `a` is older, positive when newer, zero when the same release, as a sort comparator wants (sort
|
|
79
|
+
// descending with `(a, b) => compareVersions(b, a)`). Anything that does not parse sorts below every version and
|
|
80
|
+
// equal to anything else that does not parse, so "unknown" never outranks a real release. `isNewer` above stays the
|
|
81
|
+
// numeric-only reading the update nag is built on, which treats a prerelease segment as uncertainty.
|
|
82
|
+
export const compareVersions = (a: string, b: string): number => {
|
|
83
|
+
const [left, right] = [parseVersion(a), parseVersion(b)];
|
|
84
|
+
if (left === undefined || right === undefined) {
|
|
85
|
+
return (left === undefined ? 0 : 1) - (right === undefined ? 0 : 1);
|
|
86
|
+
}
|
|
87
|
+
for (let index = 0; index < Math.max(left.parts.length, right.parts.length); index += 1) {
|
|
88
|
+
const difference = (left.parts[index] ?? 0) - (right.parts[index] ?? 0);
|
|
89
|
+
if (difference !== 0) {
|
|
90
|
+
return Math.sign(difference);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Math.sign(comparePrerelease(left.prerelease, right.prerelease));
|
|
94
|
+
};
|