@intentic/sandbox-contract 1.345.0 → 1.346.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 +26 -14
- package/dist/contracts/accounts.contract.d.ts +0 -80
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +33 -2
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +910 -29
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +43 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +2 -1
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +0 -1
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.d.ts +0 -1
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +134 -123
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +4 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +102 -1
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +0 -80
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +43 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +42 -37
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/failure-rows.d.ts +14 -0
- package/dist/events/failure-rows.d.ts.map +1 -0
- package/dist/events/failure-rows.js +109 -0
- package/dist/events/failure-rows.js.map +1 -0
- package/dist/events/sandbox-notice.d.ts +16 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +1 -0
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +68 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +7 -1
- package/dist/events/system-events.js.map +1 -1
- package/dist/index.d.ts +1158 -303
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent-lanes.d.ts.map +1 -1
- package/dist/models/agent-lanes.js +3 -5
- package/dist/models/agent-lanes.js.map +1 -1
- package/dist/models/agent-standing.d.ts +3 -20
- package/dist/models/agent-standing.d.ts.map +1 -1
- package/dist/models/agent-standing.js +12 -37
- package/dist/models/agent-standing.js.map +1 -1
- package/dist/models/model-pins.d.ts +3 -0
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js +2 -0
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/plan-pools.d.ts +23 -5
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +116 -33
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +1 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +1 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +3 -1
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +4 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +1 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/schemas/agent-next.d.ts +21 -0
- package/dist/schemas/agent-next.d.ts.map +1 -0
- package/dist/schemas/agent-next.js +21 -0
- package/dist/schemas/agent-next.js.map +1 -0
- package/dist/schemas/agent-search.d.ts +45 -0
- package/dist/schemas/agent-search.d.ts.map +1 -0
- package/dist/schemas/agent-search.js +28 -0
- package/dist/schemas/agent-search.js.map +1 -0
- package/dist/schemas/agent.d.ts +9 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +14 -10
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +16 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents.d.ts +48 -43
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +10 -33
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/compose-overlay.d.ts +9 -0
- package/dist/schemas/compose-overlay.d.ts.map +1 -0
- package/dist/schemas/compose-overlay.js +11 -0
- package/dist/schemas/compose-overlay.js.map +1 -0
- package/dist/schemas/composer-drafts.d.ts +169 -0
- package/dist/schemas/composer-drafts.d.ts.map +1 -0
- package/dist/schemas/composer-drafts.js +60 -0
- package/dist/schemas/composer-drafts.js.map +1 -0
- package/dist/schemas/device-commands.d.ts +63 -0
- package/dist/schemas/device-commands.d.ts.map +1 -0
- package/dist/schemas/device-commands.js +42 -0
- package/dist/schemas/device-commands.js.map +1 -0
- package/dist/schemas/device-folder.d.ts +11 -0
- package/dist/schemas/device-folder.d.ts.map +1 -1
- package/dist/schemas/device-folder.js +1 -0
- package/dist/schemas/device-folder.js.map +1 -1
- package/dist/schemas/devices.d.ts +14 -67
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +7 -45
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/hosts.d.ts +27 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +21 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/privacy-egress.d.ts +41 -0
- package/dist/schemas/privacy-egress.d.ts.map +1 -0
- package/dist/schemas/privacy-egress.js +30 -0
- package/dist/schemas/privacy-egress.js.map +1 -0
- package/dist/schemas/privacy.d.ts +1 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +1 -0
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +2 -130
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +5 -23
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +0 -82
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +1 -6
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/providers/refusals.d.ts +64 -0
- package/dist/schemas/providers/refusals.d.ts.map +1 -0
- package/dist/schemas/providers/refusals.js +33 -0
- package/dist/schemas/providers/refusals.js.map +1 -0
- package/dist/schemas/sessions.js +1 -1
- package/dist/schemas/sessions.js.map +1 -1
- package/dist/schemas/settings.d.ts +4 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +2 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +16 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +11 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/state/definition.d.ts +8 -0
- package/dist/state/definition.d.ts.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/sandbox-build.d.ts +20 -0
- package/dist/state/sandbox-build.d.ts.map +1 -0
- package/dist/state/sandbox-build.js +43 -0
- package/dist/state/sandbox-build.js.map +1 -0
- package/dist/state/ways-back.d.ts +4 -0
- package/dist/state/ways-back.d.ts.map +1 -0
- package/dist/state/ways-back.js +8 -0
- package/dist/state/ways-back.js.map +1 -0
- package/dist/text/land-rows.d.ts +14 -0
- package/dist/text/land-rows.d.ts.map +1 -0
- package/dist/text/land-rows.js +86 -0
- package/dist/text/land-rows.js.map +1 -0
- package/dist/text/transcript-fold.d.ts +4 -2
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -187
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +6 -6
- package/src/contracts/agents.contract.ts +49 -25
- package/src/events/agent-events.ts +95 -78
- package/src/events/failure-rows.ts +148 -0
- package/src/events/sandbox-notice.ts +11 -2
- package/src/events/system-events.ts +14 -4
- package/src/ids/project-dir.test.ts +2 -2
- package/src/ids/project-dir.ts +4 -4
- package/src/models/agent-lanes.ts +7 -21
- package/src/models/agent-runtimes.ts +2 -2
- package/src/models/agent-standing.ts +31 -96
- package/src/models/model-pins.test.ts +31 -1
- package/src/models/model-pins.ts +17 -0
- package/src/models/plan-pools.test.ts +159 -24
- package/src/models/plan-pools.ts +215 -83
- package/src/protocol/doors/runner-protocol.ts +4 -1
- package/src/protocol/raw/raw-routes.ts +1 -0
- package/src/schemas/agent-next.ts +34 -0
- package/src/schemas/agent-search.ts +51 -0
- package/src/schemas/agent.ts +21 -12
- package/src/schemas/agents.ts +24 -63
- package/src/schemas/compose-overlay.test.ts +35 -0
- package/src/schemas/compose-overlay.ts +19 -0
- package/src/schemas/composer-drafts.ts +86 -0
- package/src/schemas/device-commands.ts +89 -0
- package/src/schemas/device-folder.ts +9 -0
- package/src/schemas/devices.ts +24 -106
- package/src/schemas/environment.ts +8 -9
- package/src/schemas/hosts.ts +56 -0
- package/src/schemas/machine-presence.test.ts +33 -0
- package/src/schemas/privacy-egress.test.ts +83 -0
- package/src/schemas/privacy-egress.ts +86 -0
- package/src/schemas/privacy.ts +4 -0
- package/src/schemas/project-delivery.ts +1 -1
- package/src/schemas/providers/plan-limits.test.ts +16 -0
- package/src/schemas/providers/plan-limits.ts +8 -49
- package/src/schemas/providers/provider-oauth.ts +1 -13
- package/src/schemas/providers/refusals.ts +62 -0
- package/src/schemas/sessions.ts +1 -1
- package/src/schemas/settings.ts +2 -0
- package/src/schemas/system.ts +15 -0
- package/src/state/runtime-state.ts +5 -0
- package/src/state/sandbox-build.test.ts +95 -0
- package/src/state/sandbox-build.ts +96 -0
- package/src/state/ways-back.test.ts +36 -0
- package/src/state/ways-back.ts +19 -0
- package/src/state/workspace-state.ts +1 -1
- package/src/text/land-rows.ts +111 -0
- package/src/text/transcript-fold.test.ts +38 -9
- package/src/text/transcript-fold.ts +30 -232
- package/dist/events/undelivered.d.ts +0 -10
- package/dist/events/undelivered.d.ts.map +0 -1
- package/dist/events/undelivered.js +0 -22
- package/dist/events/undelivered.js.map +0 -1
- package/dist/state/history-state.d.ts +0 -3
- package/dist/state/history-state.d.ts.map +0 -1
- package/dist/state/history-state.js +0 -153
- package/dist/state/history-state.js.map +0 -1
- package/src/events/undelivered.ts +0 -33
- package/src/state/history-state.ts +0 -313
package/src/schemas/hosts.ts
CHANGED
|
@@ -21,6 +21,22 @@ export const MachineIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{7,1
|
|
|
21
21
|
export const derivedMachineId = (card: string): string => `card:${card}`;
|
|
22
22
|
export const isDerivedMachineId = (machineId: string): boolean => machineId.startsWith("card:");
|
|
23
23
|
|
|
24
|
+
// Which computer and which OS install on it is enrolling (`environmentKeyOf`), as the machine agent says it when it
|
|
25
|
+
// redeems a pairing. Both optional, since an agent older than either says nothing.
|
|
26
|
+
export const MachineIdentitySchema = z.object({
|
|
27
|
+
machineId: MachineIdSchema.optional(),
|
|
28
|
+
environment: z.string().min(1).optional(),
|
|
29
|
+
});
|
|
30
|
+
export type MachineIdentity = z.infer<typeof MachineIdentitySchema>;
|
|
31
|
+
|
|
32
|
+
// (2026-10-20) The body of POST /system/hosts/enroll: the redeeming agent's identity, so the enrollment is written with
|
|
33
|
+
// its real machine id instead of the card's derived one and joins that machine's sync row from its first write. Before
|
|
34
|
+
// this the route took no body and the id was learned only at the agent's first connect, which left a freshly paired
|
|
35
|
+
// computer as two machine cards until then. An agent older than it sends none, and the daemon reads one it cannot parse
|
|
36
|
+
// as none. Kept here rather than beside the answer (peer-enrollment.ts), which the browser extension bundles.
|
|
37
|
+
export const PeerEnrollmentRequestSchema = MachineIdentitySchema;
|
|
38
|
+
export type PeerEnrollmentRequest = MachineIdentity;
|
|
39
|
+
|
|
24
40
|
// How many skipped lines an agent sends in its upkeep summary at most; the counts cover the rest.
|
|
25
41
|
export const UPKEEP_SKIPPED_SHOWN = 10;
|
|
26
42
|
|
|
@@ -115,6 +131,9 @@ export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
|
|
|
115
131
|
// type a secret.
|
|
116
132
|
// - `sleep`: the `sleep` op, a sandbox put to sleep now (`ic sandbox sleep <slug>`): stopped, and started again by
|
|
117
133
|
// itself when somebody opens it; advertised only when the agent's `ic` has `sandbox sleep`.
|
|
134
|
+
// - `reachable-ways-back`: not an op but what a listing means: a sandbox's `rollbackTargets` are only the ways back a
|
|
135
|
+
// rollback can reach (ic 2026-10-19 on). Said by ic itself (`ic features`, golden/ic-features.json); without it the
|
|
136
|
+
// rows are an older ic's, which also listed pruned dev builds no download can bring back (state/ways-back.ts).
|
|
118
137
|
export const DeviceFeatureSchema = z.enum([
|
|
119
138
|
"reshape-later",
|
|
120
139
|
"set-shape",
|
|
@@ -125,6 +144,7 @@ export const DeviceFeatureSchema = z.enum([
|
|
|
125
144
|
"programs",
|
|
126
145
|
"keyring",
|
|
127
146
|
"sleep",
|
|
147
|
+
"reachable-ways-back",
|
|
128
148
|
]);
|
|
129
149
|
export type DeviceFeature = z.infer<typeof DeviceFeatureSchema>;
|
|
130
150
|
export const DEVICE_FEATURE_RESHAPE_LATER: DeviceFeature = "reshape-later";
|
|
@@ -136,6 +156,11 @@ export const DEVICE_FEATURE_PROJECT_DELIVERY: DeviceFeature = "project-delivery"
|
|
|
136
156
|
export const DEVICE_FEATURE_PROGRAMS: DeviceFeature = "programs";
|
|
137
157
|
export const DEVICE_FEATURE_KEYRING: DeviceFeature = "keyring";
|
|
138
158
|
export const DEVICE_FEATURE_SLEEP: DeviceFeature = "sleep";
|
|
159
|
+
export const DEVICE_FEATURE_REACHABLE_WAYS_BACK: DeviceFeature = "reachable-ways-back";
|
|
160
|
+
// What `ic features --json` answers (_sandbox/ic/src/features.rs, golden/ic-features.json): the optional ops that ic
|
|
161
|
+
// implements, as strings, since a newer ic's word means nothing to an older agent.
|
|
162
|
+
export const IcFeaturesAnswerSchema = z.object({ features: z.array(z.string()) });
|
|
163
|
+
|
|
139
164
|
// The features an agent advertised that this build knows; an unknown one is a newer agent's and means nothing here.
|
|
140
165
|
export const deviceFeatures = (facts: Pick<DeviceFacts, "features"> | undefined): DeviceFeature[] =>
|
|
141
166
|
(facts?.features ?? []).flatMap((feature) => {
|
|
@@ -208,6 +233,34 @@ export const HostEnvironmentSchema = z.object({
|
|
|
208
233
|
});
|
|
209
234
|
export type HostEnvironment = z.infer<typeof HostEnvironmentSchema>;
|
|
210
235
|
|
|
236
|
+
// HOW MUCH OF ONE COMPUTER ANSWERS RIGHT NOW (2026-10-20), read off its environments' own sockets: the one answer
|
|
237
|
+
// /capabilities, the Devices board and a call's routing give about a machine. `online` while its native side answers
|
|
238
|
+
// (the one named after the card, which owns the screen and every tool but `run_command`'s crossing), whatever its
|
|
239
|
+
// distros do, since a distro asleep beside a live Windows is the ordinary state of a PC; `partly` while the native side
|
|
240
|
+
// does not and another environment does (a call naming that side still reaches it); `offline` while nothing does. Before
|
|
241
|
+
// this the card read the native side alone and the board any side, so one PC was offline on one screen and live on the
|
|
242
|
+
// next (2026-10-13), and "partly" was worked out twice more. A list with no native side at all (a machine seen only
|
|
243
|
+
// through a distro's sync) is online while anything of it answers.
|
|
244
|
+
export type MachinePresence =
|
|
245
|
+
| { readonly kind: "online" }
|
|
246
|
+
| { readonly kind: "partly"; readonly awake: readonly string[] }
|
|
247
|
+
| { readonly kind: "offline" };
|
|
248
|
+
|
|
249
|
+
export const machinePresence = (environments: readonly Pick<HostEnvironment, "key" | "online">[]): MachinePresence => {
|
|
250
|
+
const native = environments.find((environment) => environment.key === HOST_NATIVE_ENVIRONMENT);
|
|
251
|
+
const awake = environments
|
|
252
|
+
.filter((environment) => environment.key !== HOST_NATIVE_ENVIRONMENT && environment.online)
|
|
253
|
+
.map((environment) => environment.key)
|
|
254
|
+
.toSorted();
|
|
255
|
+
if (native?.online === true || (native === undefined && awake.length > 0)) {
|
|
256
|
+
return { kind: "online" };
|
|
257
|
+
}
|
|
258
|
+
return awake.length > 0 ? { kind: "partly", awake } : { kind: "offline" };
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// An environment by the name a person and `run_command`'s `in` know it by: a distro by its WSL name, the rest by key.
|
|
262
|
+
export const environmentName = (key: string): string => (key.startsWith("wsl:") ? key.slice("wsl:".length) : key);
|
|
263
|
+
|
|
211
264
|
export const HostSummarySchema = z.object({
|
|
212
265
|
// The capability id, the machine's name, and the prefix of its tools (mcp__<id>__run_command). On a per-connection
|
|
213
266
|
// row (`hostConnections`) it is that connection's key instead, and `card` names the machine.
|
|
@@ -225,6 +278,9 @@ export const HostSummarySchema = z.object({
|
|
|
225
278
|
// Epoch ms of the last held socket; absent means not since this daemon booted (liveness resets on restart).
|
|
226
279
|
lastSeen: z.number().optional(),
|
|
227
280
|
facts: DeviceFactsSchema.optional(),
|
|
281
|
+
// The card's "Manage sandboxes on this device" switch is on: a container verb sent to this machine (create, start,
|
|
282
|
+
// remove…) is let through rather than refused by the machine. Absent from a daemon older than it.
|
|
283
|
+
manages: z.boolean().optional(),
|
|
228
284
|
});
|
|
229
285
|
export type HostSummary = z.infer<typeof HostSummarySchema>;
|
|
230
286
|
export const HostsListSchema = z.object({ hosts: z.array(HostSummarySchema) });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { environmentName, machinePresence } from "./hosts.js";
|
|
2
|
+
|
|
3
|
+
// One answer to "does this computer answer", read by /capabilities, the Devices board and a call's routing alike
|
|
4
|
+
// (2026-10-13/15: one PC was offline on the card, live on the board, and partly connected by a third reading).
|
|
5
|
+
|
|
6
|
+
const side = (key: string, online: boolean) => ({ key, online });
|
|
7
|
+
|
|
8
|
+
test("a PC whose native side answers is online, whatever its distros do", () => {
|
|
9
|
+
expect(machinePresence([side("native", true), side("wsl:archlinux", false)])).toEqual({ kind: "online" });
|
|
10
|
+
expect(machinePresence([side("native", true), side("wsl:archlinux", true)])).toEqual({ kind: "online" });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("a PC answering only through its distros is partly connected, and names them in order", () => {
|
|
14
|
+
expect(machinePresence([side("native", false), side("wsl:ubuntu", true), side("wsl:archlinux", true)])).toEqual({
|
|
15
|
+
kind: "partly",
|
|
16
|
+
awake: ["wsl:archlinux", "wsl:ubuntu"],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a computer nothing of which answers is offline", () => {
|
|
21
|
+
expect(machinePresence([side("native", false), side("wsl:archlinux", false)])).toEqual({ kind: "offline" });
|
|
22
|
+
expect(machinePresence([])).toEqual({ kind: "offline" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// A machine seen only through a distro's sync has no native side to be missing.
|
|
26
|
+
test("a computer with no native side is online while anything of it answers", () => {
|
|
27
|
+
expect(machinePresence([side("wsl:archlinux", true)])).toEqual({ kind: "online" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("names a distro the way WSL and `in` do", () => {
|
|
31
|
+
expect(environmentName("wsl:archlinux")).toBe("archlinux");
|
|
32
|
+
expect(environmentName("native")).toBe("native");
|
|
33
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { DEFAULT_PRIVACY_SHIELD, type PrivacyShieldMode, type PrivacyShieldPolicy, withConversationTrust } from "./privacy.js";
|
|
2
|
+
import { isPrivacyRefusal, ownerTrusted, type PrivacyEgress, type PrivacyRoute, shieldVerdict, turnRoute } from "./privacy-egress.js";
|
|
3
|
+
|
|
4
|
+
// One verdict for every place bytes leave for a model, and for the editor's word before a send. Each scenario the shield
|
|
5
|
+
// once got wrong at one of those places is a row here, so a place that reads the verdict cannot get it wrong again.
|
|
6
|
+
|
|
7
|
+
const policy = (mode: PrivacyShieldMode, extra: Partial<PrivacyShieldPolicy> = {}): PrivacyShieldPolicy => ({ ...DEFAULT_PRIVACY_SHIELD, mode, ...extra });
|
|
8
|
+
const egress = (route: PrivacyRoute, extra: Partial<PrivacyEgress> = {}): PrivacyEgress => ({ provider: "cursor", local: false, route, ...extra });
|
|
9
|
+
|
|
10
|
+
const ROUTES: readonly PrivacyRoute[] = ["gateway", "hooks", "sealed", "runtime", "runner", "own-key", "shared-server"];
|
|
11
|
+
|
|
12
|
+
describe(`shieldVerdict`, () => {
|
|
13
|
+
it(`passes everything while the shield is off, and only reads while it watches`, () => {
|
|
14
|
+
for (const route of ROUTES) {
|
|
15
|
+
expect(shieldVerdict(policy(`off`), egress(route))).toEqual({ kind: `pass`, because: `off` });
|
|
16
|
+
}
|
|
17
|
+
expect(ROUTES.map((route) => shieldVerdict(policy(`watch`), egress(route)).kind)).toEqual([
|
|
18
|
+
`watch`,
|
|
19
|
+
`watch`,
|
|
20
|
+
`watch`,
|
|
21
|
+
`pass`,
|
|
22
|
+
`pass`,
|
|
23
|
+
`pass`,
|
|
24
|
+
`pass`,
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it(`masks every seam and refuses every bypass while it is on`, () => {
|
|
29
|
+
expect(ROUTES.map((route) => shieldVerdict(policy(`on`), egress(route)))).toEqual([
|
|
30
|
+
{ kind: `mask` },
|
|
31
|
+
{ kind: `mask` },
|
|
32
|
+
{ kind: `mask` },
|
|
33
|
+
{ kind: `refuse`, route: `runtime` },
|
|
34
|
+
{ kind: `refuse`, route: `runner` },
|
|
35
|
+
{ kind: `refuse`, route: `own-key` },
|
|
36
|
+
{ kind: `refuse`, route: `shared-server` },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it(`passes a local model, a trusted provider and a granted conversation on every route`, () => {
|
|
41
|
+
const granted = withConversationTrust(policy(`on`), `c-1`, `cursor`, true);
|
|
42
|
+
for (const route of ROUTES) {
|
|
43
|
+
expect(shieldVerdict(policy(`on`), egress(route, { local: true }))).toEqual({ kind: `pass`, because: `local` });
|
|
44
|
+
expect(shieldVerdict(policy(`on`, { trusted: [`cursor`] }), egress(route))).toEqual({ kind: `pass`, because: `trusted` });
|
|
45
|
+
expect(shieldVerdict(granted, egress(route, { conversationId: `c-1` }))).toEqual({ kind: `pass`, because: `granted` });
|
|
46
|
+
// The grant is that conversation's alone, and a request outside any conversation (a helper) has none.
|
|
47
|
+
expect(shieldVerdict(granted, egress(route, { conversationId: `c-2` })).kind).not.toBe(`pass`);
|
|
48
|
+
expect(shieldVerdict(granted, egress(route)).kind).not.toBe(`pass`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// grand-mesa-dsjg (2026-10-07): commit messages on Composer were refused before a word was read, while a turn on the
|
|
53
|
+
// same provider ran. A helper's prompt is sealed, so it is read and masked like a turn's, never refused for its runtime.
|
|
54
|
+
it(`reads a helper's sealed prompt exactly as a hooked turn is read`, () => {
|
|
55
|
+
expect(shieldVerdict(policy(`on`), egress(`sealed`))).toEqual(shieldVerdict(policy(`on`), egress(`hooks`)));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// calm-lichen-5wrn (2026-10-08): opening a Cursor composer warned before anything was sent. A hooked runtime is a seam.
|
|
59
|
+
// deft-shale-590s (2026-10-11): the same conversation on a runner was refused only after the send.
|
|
60
|
+
it(`refuses a turn by its placement, not by its runtime`, () => {
|
|
61
|
+
expect(shieldVerdict(policy(`on`), egress(turnRoute(`hooks`)))).toEqual({ kind: `mask` });
|
|
62
|
+
expect(shieldVerdict(policy(`on`), egress(turnRoute(`hooks`, `omen`)))).toEqual({ kind: `refuse`, route: `runner` });
|
|
63
|
+
expect(shieldVerdict(policy(`on`), egress(turnRoute(`gateway`, `omen`)))).toEqual({ kind: `refuse`, route: `runner` });
|
|
64
|
+
expect(shieldVerdict(policy(`on`), egress(turnRoute(`none`)))).toEqual({ kind: `refuse`, route: `runtime` });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it(`calls only the owner's own choices trusted`, () => {
|
|
68
|
+
expect(ownerTrusted({ kind: `pass`, because: `granted` })).toBe(true);
|
|
69
|
+
expect(ownerTrusted({ kind: `pass`, because: `local` })).toBe(true);
|
|
70
|
+
expect(ownerTrusted({ kind: `pass`, because: `off` })).toBe(false);
|
|
71
|
+
expect(ownerTrusted({ kind: `pass`, because: `unwatched` })).toBe(false);
|
|
72
|
+
expect(ownerTrusted({ kind: `mask` })).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe(`isPrivacyRefusal`, () => {
|
|
77
|
+
it(`knows the shield's own refusals and nothing else`, () => {
|
|
78
|
+
expect(isPrivacyRefusal(`privacy-unshielded`)).toBe(true);
|
|
79
|
+
expect(isPrivacyRefusal(`privacy-instructions`)).toBe(true);
|
|
80
|
+
expect(isPrivacyRefusal(`model-unavailable`)).toBe(false);
|
|
81
|
+
expect(isPrivacyRefusal(undefined)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type PrivacyShieldPolicy, trustedInConversation } from "./privacy.js";
|
|
3
|
+
|
|
4
|
+
// THE ONE DECISION about bytes bound for a model provider: pass them as they are, watch them, mask them, or refuse them.
|
|
5
|
+
// Every place they leave by asks it (the gateway, a hooked runtime's channels, a sealed helper prompt, a turn's
|
|
6
|
+
// admission, a runner's placement, a credential that would go around the gateway), and so does the editor before a
|
|
7
|
+
// send, so the warning above the composer is this verdict and never a guess of its own. Before 2026-10-20 the formula
|
|
8
|
+
// was written out at each of those places, and they drifted: helpers refused where turns ran, a child kept here where
|
|
9
|
+
// its parent could go, a strip that warned on opening a composer and stayed silent about a runner.
|
|
10
|
+
|
|
11
|
+
// How bytes reach the provider. Through a seam the shield reads every byte at: the gateway a runtime is pointed at, the
|
|
12
|
+
// hooks a runtime hands each channel's text to before its model reads it, a sealed prompt (a helper's whole request, in
|
|
13
|
+
// hand before any runtime has it). Or past every seam: a runtime with neither (an ACP agent, Pi), a runner calling its
|
|
14
|
+
// provider from its own machine, a key the provider is called with directly (Codex on the container's OpenAI key), a
|
|
15
|
+
// shared server started before the shield was on.
|
|
16
|
+
export type PrivacySeam = "gateway" | "hooks" | "sealed";
|
|
17
|
+
export const PrivacyBypassSchema = z.enum(["runtime", "runner", "own-key", "shared-server"]);
|
|
18
|
+
export type PrivacyBypass = z.infer<typeof PrivacyBypassSchema>;
|
|
19
|
+
export type PrivacyRoute = PrivacySeam | PrivacyBypass;
|
|
20
|
+
|
|
21
|
+
const SEAMS: ReadonlySet<PrivacyRoute> = new Set<PrivacySeam>(["gateway", "hooks", "sealed"]);
|
|
22
|
+
const isSeam = (route: PrivacyRoute): route is PrivacySeam => SEAMS.has(route);
|
|
23
|
+
|
|
24
|
+
// The route of a turn on a runtime, from its `privacy` capability (agent-runtimes.ts), placed on `runner` or here.
|
|
25
|
+
export const turnRoute = (privacy: "gateway" | "hooks" | "none", runner?: string): PrivacyRoute =>
|
|
26
|
+
runner !== undefined ? "runner" : privacy === "none" ? "runtime" : privacy;
|
|
27
|
+
|
|
28
|
+
export interface PrivacyEgress {
|
|
29
|
+
readonly provider: string;
|
|
30
|
+
// A model this machine serves: nothing it reads leaves.
|
|
31
|
+
readonly local: boolean;
|
|
32
|
+
// A conversation's own grant counts there; a request outside any conversation has none.
|
|
33
|
+
readonly conversationId?: string | undefined;
|
|
34
|
+
readonly route: PrivacyRoute;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Why bytes pass as they are: the shield is off, the model is on this machine, the owner trusts the provider everywhere
|
|
38
|
+
// or let it read this conversation, or the shield only watches and cannot see this route at all.
|
|
39
|
+
export type PassReason = "off" | "local" | "trusted" | "granted" | "unwatched";
|
|
40
|
+
|
|
41
|
+
export type ShieldVerdict =
|
|
42
|
+
| { readonly kind: "pass"; readonly because: PassReason }
|
|
43
|
+
| { readonly kind: "watch" }
|
|
44
|
+
| { readonly kind: "mask" }
|
|
45
|
+
| { readonly kind: "refuse"; readonly route: PrivacyBypass };
|
|
46
|
+
|
|
47
|
+
const pass = (because: PassReason): ShieldVerdict => ({ kind: "pass", because });
|
|
48
|
+
|
|
49
|
+
export const shieldVerdict = (policy: Pick<PrivacyShieldPolicy, "mode" | "trusted" | "conversations">, egress: PrivacyEgress): ShieldVerdict => {
|
|
50
|
+
if (policy.mode === "off") {
|
|
51
|
+
return pass("off");
|
|
52
|
+
}
|
|
53
|
+
if (egress.local) {
|
|
54
|
+
return pass("local");
|
|
55
|
+
}
|
|
56
|
+
if (policy.trusted.includes(egress.provider)) {
|
|
57
|
+
return pass("trusted");
|
|
58
|
+
}
|
|
59
|
+
if (trustedInConversation(policy, egress.provider, egress.conversationId)) {
|
|
60
|
+
return pass("granted");
|
|
61
|
+
}
|
|
62
|
+
if (!isSeam(egress.route)) {
|
|
63
|
+
return policy.mode === "on" ? { kind: "refuse", route: egress.route } : pass("unwatched");
|
|
64
|
+
}
|
|
65
|
+
return policy.mode === "on" ? { kind: "mask" } : { kind: "watch" };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Whether the owner let the provider read what it is sent as it is (here, everywhere, or in this conversation): what
|
|
69
|
+
// the log calls trusted.
|
|
70
|
+
export const ownerTrusted = (verdict: ShieldVerdict): boolean =>
|
|
71
|
+
verdict.kind === "pass" && (verdict.because === "local" || verdict.because === "trusted" || verdict.because === "granted");
|
|
72
|
+
|
|
73
|
+
// The failure codes a turn the shield turned away ends with: refused by its verdict (`privacy-unshielded`), or for what
|
|
74
|
+
// it found where it could not mask (`privacy-instructions`). The way through both is the same grant.
|
|
75
|
+
const REFUSAL_CODES = ["privacy-unshielded", "privacy-instructions"] as const;
|
|
76
|
+
export type PrivacyRefusalCode = (typeof REFUSAL_CODES)[number];
|
|
77
|
+
const REFUSAL_CODE_SET: ReadonlySet<string> = new Set(REFUSAL_CODES);
|
|
78
|
+
export const isPrivacyRefusal = (code: string | undefined): code is PrivacyRefusalCode => code !== undefined && REFUSAL_CODE_SET.has(code);
|
|
79
|
+
|
|
80
|
+
// A refusal as the shield both words and types it: the sentence a turn ends with, and the route its verdict refused,
|
|
81
|
+
// which rides the turn's error frame (`privacyRoute`) so a reader acts on the route rather than on the sentence. Absent
|
|
82
|
+
// for a policy that could not be read, which refuses whatever the route.
|
|
83
|
+
export interface PrivacyRefusal {
|
|
84
|
+
readonly sentence: string;
|
|
85
|
+
readonly route?: PrivacyBypass;
|
|
86
|
+
}
|
package/src/schemas/privacy.ts
CHANGED
|
@@ -42,6 +42,10 @@ export type PrivacyNames = z.infer<typeof PrivacyNamesSchema>;
|
|
|
42
42
|
|
|
43
43
|
export const PRIVACY_TRUSTED_MAX = 200;
|
|
44
44
|
export const PRIVACY_ALLOW_MAX = 1000;
|
|
45
|
+
|
|
46
|
+
// How a never-masked value (policy `allow`) is compared with what the shield finds, by the detectors and by the editor
|
|
47
|
+
// that keeps the list: case, Unicode composition and runs of whitespace do not tell two values apart.
|
|
48
|
+
export const allowKey = (value: string): string => value.normalize("NFC").toLowerCase().replace(/\s+/g, " ").trim();
|
|
45
49
|
export const PRIVACY_CONVERSATIONS_MAX = 200;
|
|
46
50
|
|
|
47
51
|
// The owner's own additions to what the shield finds. The policy travels whole on every write, so each is kept to what a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
// LANDED WORK DELIVERED TO THE OWNER'S FOLDER. A folder
|
|
3
|
+
// LANDED WORK DELIVERED TO THE OWNER'S FOLDER. A folder with a sandbox made for it is copied one way into
|
|
4
4
|
// `/work/<name>`; when a conversation's work lands there, the daemon hands the same change to the machine agent that
|
|
5
5
|
// holds the folder, which applies it file by file under a restore point and never over an edit of the owner's own.
|
|
6
6
|
// The daemon asks (`deliverProject` on the device contract), the machine decides and writes; nothing an agent runs can
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sandboxAnswerSchema } from "../../index.js";
|
|
2
|
+
import { ProviderRefusalsSchema } from "./plan-limits.js";
|
|
3
|
+
|
|
4
|
+
// 2026-10-21: /agent/refusals went from one refusal per provider to a list per provider, and the editor parses every
|
|
5
|
+
// answer with its own build's schema. Against a released daemon (1.249.0 to 1.345.0) the parse failed, the read was kept
|
|
6
|
+
// as nothing, and every spent account and model read as ready.
|
|
7
|
+
test("reads a released daemon's one refusal per provider as a list of one, and today's lists as they are", () => {
|
|
8
|
+
const spent = { at: 1_760_000_000_000, kind: "limit", message: "Claude AI usage limit reached", account: "acct-1", resetsAt: 1_760_010_000 };
|
|
9
|
+
const schema = sandboxAnswerSchema(["agent", "refusals"]);
|
|
10
|
+
const older = schema?.safeParse({ refusals: { claude: spent } });
|
|
11
|
+
expect(older?.success).toBe(true);
|
|
12
|
+
expect(older?.data).toEqual({ refusals: { claude: [spent] } });
|
|
13
|
+
const today = { refusals: { claude: [spent, { at: 1_760_000_000_001, kind: "auth", message: "OAuth token revoked", account: "acct-2" }], codex: [] } };
|
|
14
|
+
expect(schema?.safeParse(today).data).toEqual(today);
|
|
15
|
+
expect(ProviderRefusalsSchema.safeParse({ refusals: { claude: "spent" } }).success).toBe(false);
|
|
16
|
+
});
|
|
@@ -86,9 +86,9 @@ export type AccountUsage = z.infer<typeof AccountUsageSchema>;
|
|
|
86
86
|
// words for a person, never a key.
|
|
87
87
|
export const AccountFixSchema = z.enum(["reconnect", "admin", "verify", "wait"]);
|
|
88
88
|
export type AccountFix = z.infer<typeof AccountFixSchema>;
|
|
89
|
-
// Whether an account can serve a turn,
|
|
90
|
-
//
|
|
91
|
-
//
|
|
89
|
+
// Whether an account can serve a turn, judged by one rule (models/plan-pools.ts `serviceStates`) from every fact on file
|
|
90
|
+
// about it: a revoked sign-in, a lost seat, a translator bench, every refusal still standing, and its plan limits. The
|
|
91
|
+
// daemon and the editor run that rule on the same facts; nothing publishes or keeps a judged copy.
|
|
92
92
|
export const AccountStateSchema = z.discriminatedUnion("kind", [
|
|
93
93
|
z.object({
|
|
94
94
|
kind: z.literal("ready"),
|
|
@@ -104,6 +104,10 @@ export const AccountStateSchema = z.discriminatedUnion("kind", [
|
|
|
104
104
|
.number()
|
|
105
105
|
.optional()
|
|
106
106
|
.describe("When every full pool has reopened, in epoch seconds, where the plan publishes it. Absent means unknown, never now."),
|
|
107
|
+
pool: z
|
|
108
|
+
.string()
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("The pool holding it shut, by the plan's own name for it, where the plan meters it apart from other models. Absent for an undivided allowance."),
|
|
107
111
|
}),
|
|
108
112
|
z.object({
|
|
109
113
|
kind: z.literal("blocked"),
|
|
@@ -145,49 +149,7 @@ export const LimitResetClaimSchema = z.object({
|
|
|
145
149
|
detail: z.string().optional().describe("What went wrong, in words, for the two outcomes that are this sandbox's fault rather than the plan's."),
|
|
146
150
|
});
|
|
147
151
|
export type LimitResetClaim = z.infer<typeof LimitResetClaimSchema>;
|
|
148
|
-
|
|
149
|
-
// turn doesn't know which served it. `kind` is read off what the provider said, not the frame code.
|
|
150
|
-
export const ProviderRefusalSchema = z.object({
|
|
151
|
-
// Read side by side with `AccountUsage.measuredAt` (also ms).
|
|
152
|
-
at: z.number().describe("When it refused, in milliseconds."),
|
|
153
|
-
// Only a turn that actually runs settles an entitlement refusal — nothing else can.
|
|
154
|
-
kind: z
|
|
155
|
-
.enum(["limit", "auth", "entitlement"])
|
|
156
|
-
.describe(
|
|
157
|
-
"Three different noes, kept apart because what fixes each is different. A spent allowance is answered by waiting; a refused credential by signing in again; and an entitlement refusal, where somebody has switched this off for your seat, by neither of those. That last one authenticates fine and reports healthy limits the whole time it refuses everything.",
|
|
158
|
-
),
|
|
159
|
-
message: z.string().describe("The provider's own words, verbatim. The only part that says which limit or which credential."),
|
|
160
|
-
// Known for native turns only; a routed turn doesn't say which account served it.
|
|
161
|
-
account: z.string().optional().describe("Which account was serving, where that is known."),
|
|
162
|
-
// Lets a `limit` refusal be read against the pool that model spends (`UsageWindow.gates`), not the account's
|
|
163
|
-
// fullest pool.
|
|
164
|
-
model: z.string().optional().describe("Which model the refused turn was on, where that is known."),
|
|
165
|
-
// The provider's own "try again at …", which a polled reading cannot contradict: a usage endpoint can keep showing
|
|
166
|
-
// room while the plan refuses every turn.
|
|
167
|
-
resetsAt: z
|
|
168
|
-
.number()
|
|
169
|
-
.optional()
|
|
170
|
-
.describe(
|
|
171
|
-
"When the provider said to try again, in epoch seconds, for a spent allowance where it named one. Until then the refusal stands whatever a reading says; after it, it is over.",
|
|
172
|
-
),
|
|
173
|
-
// Cursor's "ask your admin to increase your limit": a team's spend limit, which no usage endpoint reads. Not Codex's
|
|
174
|
-
// "send a request to your admin", which names a window its reading measures (failure-sentences.ts ADMIN_GRANTED).
|
|
175
|
-
unmetered: z
|
|
176
|
-
.literal(true)
|
|
177
|
-
.optional()
|
|
178
|
-
.describe(
|
|
179
|
-
"Set on a spent allowance no usage reading measures, such as a credit an organisation's admin grants. No reading answers it: it stands until the provider's own reset, or until a turn is answered.",
|
|
180
|
-
),
|
|
181
|
-
});
|
|
182
|
-
export type ProviderRefusal = z.infer<typeof ProviderRefusalSchema>;
|
|
183
|
-
export const ProviderRefusalsSchema = z.object({
|
|
184
|
-
refusals: z
|
|
185
|
-
.record(z.string(), ProviderRefusalSchema)
|
|
186
|
-
.describe(
|
|
187
|
-
"The most recent refusal per provider. Read alongside an account's usage: that says how full it was when last checked, this says whether it has since started saying no.",
|
|
188
|
-
),
|
|
189
|
-
});
|
|
190
|
-
export type ProviderRefusals = z.infer<typeof ProviderRefusalsSchema>;
|
|
152
|
+
export { type ProviderRefusal, ProviderRefusalSchema, type ProviderRefusals, ProviderRefusalsSchema } from "./refusals.js";
|
|
191
153
|
// `name` is CLIProxyAPI's auth-file name, the stable key a disconnect addresses; `label` is the reported sign-in
|
|
192
154
|
// identity (email, else the file name).
|
|
193
155
|
export const TranslatorAccountSchema = z.object({
|
|
@@ -212,9 +174,6 @@ export const TranslatorAccountSchema = z.object({
|
|
|
212
174
|
verify: z.string().optional(),
|
|
213
175
|
})
|
|
214
176
|
.optional(),
|
|
215
|
-
state: AccountStateSchema.optional().describe(
|
|
216
|
-
"Whether it can serve a turn now, judged from everything above plus the provider's last refusal. Absent from a daemon older than this field.",
|
|
217
|
-
),
|
|
218
177
|
});
|
|
219
178
|
export type TranslatorAccount = z.infer<typeof TranslatorAccountSchema>;
|
|
220
179
|
// A list per provider, not a flag: CLIProxyAPI holds several auth files per provider and balances across them. Built
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { AccountUsageSchema } from "./plan-limits.js";
|
|
3
3
|
import { SignInCatcherSchema } from "../loopback-catch.js";
|
|
4
4
|
import { KeyedProviderSchema } from "./provider-subscriptions.js";
|
|
5
5
|
// Claude uses PKCE paste-back (start → exchange); Codex uses OpenAI's device-code flow (start → poll). A sandbox can
|
|
@@ -38,9 +38,6 @@ export const OauthAccountSchema = z.object({
|
|
|
38
38
|
usage: AccountUsageSchema.optional().describe(
|
|
39
39
|
"How full its plan limits were when last measured, so a picker can show what is left before committing work to it. Absent until a reading exists, which reads as unknown rather than as nothing left.",
|
|
40
40
|
),
|
|
41
|
-
state: AccountStateSchema.optional().describe(
|
|
42
|
-
"Whether it can serve a turn now, judged once from the sign-in, the seat, the provider's last refusal and the plan limits: the verdict every picker in the sandbox uses. Read this rather than the fields it was judged from. Absent from a daemon older than this field.",
|
|
43
|
-
),
|
|
44
41
|
});
|
|
45
42
|
export type OauthAccount = z.infer<typeof OauthAccountSchema>;
|
|
46
43
|
export const OauthAccountListSchema = z.object({
|
|
@@ -191,15 +188,6 @@ export const ModelSchema = z.object({
|
|
|
191
188
|
helperOnly: HelperOnlySchema.optional().describe(
|
|
192
189
|
"Set where this model may write commit messages, titles and other one-shot jobs but never run a chat turn, and why: its server says it cannot call tools, or it is the small local model kept for quick jobs. Absent means nothing has said it cannot.",
|
|
193
190
|
),
|
|
194
|
-
// Epoch seconds. A fact about the MODEL, not about any one account: a routed provider balances across credentials,
|
|
195
|
-
// so a model every one of them is benched on is unrunnable however much headroom their rings show. Absent is the
|
|
196
|
-
// ordinary case and means runnable, never "unknown".
|
|
197
|
-
availableAt: z
|
|
198
|
-
.number()
|
|
199
|
-
.optional()
|
|
200
|
-
.describe(
|
|
201
|
-
"When this model can be asked again, where every credential that serves it is currently refused. Absent means it can be asked now. A model here is still worth showing, unlike one the plan does not cover at all: the wait is the whole answer.",
|
|
202
|
-
),
|
|
203
191
|
});
|
|
204
192
|
export type Model = z.infer<typeof ModelSchema>;
|
|
205
193
|
export const ModelsSchema = z.object({
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// WHAT A PROVIDER SAID WHEN IT REFUSED A TURN, and the ledger of them the sandbox answers with. Kept out of plan-limits.ts,
|
|
4
|
+
// which re-exports both, so the plan and turn schemas there stay within one file's reach.
|
|
5
|
+
|
|
6
|
+
// Observed and exact, unlike a polled usage snapshot that can be stale; keyed by provider, not account, since a routed
|
|
7
|
+
// turn doesn't know which served it. `kind` is read off what the provider said, not the frame code.
|
|
8
|
+
export const ProviderRefusalSchema = z.object({
|
|
9
|
+
// Read side by side with `AccountUsage.measuredAt` (also ms).
|
|
10
|
+
at: z.number().describe("When it refused, in milliseconds."),
|
|
11
|
+
// Only a turn that actually runs settles an entitlement refusal — nothing else can.
|
|
12
|
+
kind: z
|
|
13
|
+
.enum(["limit", "auth", "entitlement"])
|
|
14
|
+
.describe(
|
|
15
|
+
"Three different noes, kept apart because what fixes each is different. A spent allowance is answered by waiting; a refused credential by signing in again; and an entitlement refusal, where somebody has switched this off for your seat, by neither of those. That last one authenticates fine and reports healthy limits the whole time it refuses everything.",
|
|
16
|
+
),
|
|
17
|
+
message: z.string().describe("The provider's own words, verbatim. The only part that says which limit or which credential."),
|
|
18
|
+
// Known for native turns only; a routed turn doesn't say which account served it.
|
|
19
|
+
account: z.string().optional().describe("Which account was serving, where that is known."),
|
|
20
|
+
// Lets a `limit` refusal be read against the pool that model spends (`UsageWindow.gates`), not the account's
|
|
21
|
+
// fullest pool.
|
|
22
|
+
model: z.string().optional().describe("Which model the refused turn was on, where that is known."),
|
|
23
|
+
// The provider's own "try again at …", which a polled reading cannot contradict: a usage endpoint can keep showing
|
|
24
|
+
// room while the plan refuses every turn.
|
|
25
|
+
resetsAt: z
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
"When the provider said to try again, in epoch seconds, for a spent allowance where it named one. Until then the refusal stands whatever a reading says; after it, it is over.",
|
|
30
|
+
),
|
|
31
|
+
// Stamped by the daemon's ledger as it files a spent allowance naming no instant (plan-pools.ts refusedPoolReset).
|
|
32
|
+
poolResetsAt: z.number().optional().describe("Epoch seconds the refused pool reopens, for a spent allowance naming no time: it ends then."),
|
|
33
|
+
// Cursor's "ask your admin to increase your limit": a team's spend limit, which no usage endpoint reads. Not Codex's
|
|
34
|
+
// "send a request to your admin", which names a window its reading measures (failure-sentences.ts ADMIN_GRANTED).
|
|
35
|
+
unmetered: z
|
|
36
|
+
.literal(true)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(
|
|
39
|
+
"Set on a spent allowance no usage reading measures, such as a credit an organisation's admin grants. No reading answers it: it stands until the provider's own reset, or until a turn is answered.",
|
|
40
|
+
),
|
|
41
|
+
byHand: z
|
|
42
|
+
.literal(true)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
"Set by a person rather than by a refused turn, for a plan nothing can read: believed for a day, or until unmarked or a turn on it is answered.",
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
export type ProviderRefusal = z.infer<typeof ProviderRefusalSchema>;
|
|
49
|
+
// A released daemon (1.345.0 and earlier) answers one refusal per provider, its last: read as a list of one, as the
|
|
50
|
+
// /events frame's `refusal` is (refusalsOf). What this build sends, and its output shape, is still the list.
|
|
51
|
+
const RefusalsOnFileSchema = z.codec(z.union([z.array(ProviderRefusalSchema), ProviderRefusalSchema]), z.array(ProviderRefusalSchema), {
|
|
52
|
+
decode: (filed) => (Array.isArray(filed) ? filed : [filed]),
|
|
53
|
+
encode: (list) => list,
|
|
54
|
+
});
|
|
55
|
+
export const ProviderRefusalsSchema = z.object({
|
|
56
|
+
refusals: z
|
|
57
|
+
.record(z.string(), RefusalsOnFileSchema)
|
|
58
|
+
.describe(
|
|
59
|
+
"Every refusal on file per provider, each the latest of its kind for the account and model it names: what every verdict on that provider's accounts is judged against. Read alongside an account's usage: that says how full it was when last checked, these say whether it has since started saying no.",
|
|
60
|
+
),
|
|
61
|
+
});
|
|
62
|
+
export type ProviderRefusals = z.infer<typeof ProviderRefusalsSchema>;
|
package/src/schemas/sessions.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { MatchSnippetSchema } from "./
|
|
2
|
+
import { MatchSnippetSchema } from "./agent-search.js";
|
|
3
3
|
export const SessionIdParamSchema = z.object({ id: z.string().describe("Which past conversation.") });
|
|
4
4
|
export const SessionSummarySchema = z.object({
|
|
5
5
|
id: z.string().describe("Its id."),
|
package/src/schemas/settings.ts
CHANGED
|
@@ -602,6 +602,8 @@ export const SandboxSettingsSchema = z.object({
|
|
|
602
602
|
childRerunHorizonHours: z.number().int().min(0).max(24).default(6).describe(
|
|
603
603
|
"How long a subagent refused for a spent allowance waits for the provider's reset before it goes again by itself. Past it, or at 0, it stops and its parent hears where else the work can go. One spawned with fallbacks waits at most 90 minutes and then moves to them.",
|
|
604
604
|
),
|
|
605
|
+
subagentBudget: z.boolean().default(true).describe("Start a subagent only where an account's allowance can carry it through its windows, on the account left with the most room after it. The rest wait as pending and start by themselves as running ones end or a window reopens. Off, every subagent starts at once, wherever the most room reads now."),
|
|
606
|
+
subagentReservePercent: z.number().int().min(0).max(50).default(10).describe("How much of each allowance window, in points of 100, subagents leave for your own conversations: a fan-out is planned against what is left past it."),
|
|
605
607
|
// Width again, for a team-chat automation rather than a turn: each thread a message lands in is a conversation of
|
|
606
608
|
// its own, and threads of one automation are worked on side by side up to this many (automations/scheduler.ts in
|
|
607
609
|
// the daemon); one more waits, told it is queued, until a slot frees. Read per fire, so a change holds from the next
|
package/src/schemas/system.ts
CHANGED
|
@@ -61,6 +61,17 @@ export const PreparingUpdateSchema = z.object({
|
|
|
61
61
|
percent: z.number().min(0).max(100).optional().describe("How far through the download it is, from 0 to 100. Absent for a step with no measure of its own."),
|
|
62
62
|
});
|
|
63
63
|
export type PreparingUpdate = z.infer<typeof PreparingUpdateSchema>;
|
|
64
|
+
// What answers (state/sandbox-build.ts RunningSandbox): the build this container runs and when its daemon started.
|
|
65
|
+
export const RunningSandboxSchema = z.object({
|
|
66
|
+
build: z
|
|
67
|
+
.object({
|
|
68
|
+
base: z.string().describe("The base image as the machine that runs it named it; empty when it said nothing."),
|
|
69
|
+
baseDigest: z.string().optional().describe("That base's registry digest, when the machine read one."),
|
|
70
|
+
overlay: z.string().optional().describe("The hash of the environment recipe built on the base. Absent on a stock image."),
|
|
71
|
+
})
|
|
72
|
+
.describe("Which build this container is: its base image and the environment built on it."),
|
|
73
|
+
bootedAt: z.number().describe("When the daemon answering started, in milliseconds. A container swapped in is a new boot."),
|
|
74
|
+
});
|
|
64
75
|
export const InfoSchema = z.object({
|
|
65
76
|
name: z.string().optional().describe("What this sandbox is called."),
|
|
66
77
|
image: z.string().optional().describe("The image it is running."),
|
|
@@ -129,6 +140,10 @@ export const InfoSchema = z.object({
|
|
|
129
140
|
autoUpdate: AutoUpdateSchema.optional().describe(
|
|
130
141
|
"Whether and when this sandbox takes a downloaded update by itself, and what it is waiting on. Absent where it cannot: a hosted sandbox, one built from a checkout, or one older than automatic updates.",
|
|
131
142
|
),
|
|
143
|
+
// What answers now, for a page telling a swap it pressed from one still running; absent from a daemon older than it.
|
|
144
|
+
running: RunningSandboxSchema.optional().describe(
|
|
145
|
+
"Which build answers and when its daemon started, so a page that asked for a rebuild, an update or a rollback can tell when the new container is the one answering. Absent from a sandbox older than this field.",
|
|
146
|
+
),
|
|
132
147
|
});
|
|
133
148
|
export type Info = z.infer<typeof InfoSchema>;
|
|
134
149
|
// What the daemon could not read in its own `.intentic/` state files, and in the files on its volume that
|
|
@@ -73,6 +73,11 @@ const RUNTIME_DOMAINS = [
|
|
|
73
73
|
// A standing yes written or taken back outside a need's answer: a permission card's pass, a release, a purge. They
|
|
74
74
|
// live under the auth root, which no file push watches, and the Allowed list must move with the card that wrote one.
|
|
75
75
|
{ domain: "grants", invalidates: [["grants"]] },
|
|
76
|
+
|
|
77
|
+
// Somebody's composer draft was written, cleared or sent. The frame names no conversation and carries no words, since
|
|
78
|
+
// every member hears it: each window reads back its own caller's drafts (agents.drafts). The key is the editor's draft
|
|
79
|
+
// store's, which reads them back itself rather than through a cached query (models/conversations/drafts/).
|
|
80
|
+
{ domain: "drafts", invalidates: [["drafts"]] },
|
|
76
81
|
] as const satisfies readonly RuntimeDomainBinding[];
|
|
77
82
|
|
|
78
83
|
export const RUNTIME_DOMAIN_BINDINGS: readonly RuntimeDomainBinding[] = RUNTIME_DOMAINS;
|