@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,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
bindingWindow,
|
|
3
|
+
currentWindows,
|
|
3
4
|
gatesModel,
|
|
4
5
|
gatingWindows,
|
|
5
6
|
headroomState,
|
|
@@ -116,6 +117,38 @@ test("a reading is live until its reset, or, with none published, for one window
|
|
|
116
117
|
expect(windowLive(window({ kind: "claude:tangelo" }), NOW - 400 * HOUR, NOW)).toBe(true);
|
|
117
118
|
});
|
|
118
119
|
|
|
120
|
+
test("a pool whose window closed since its reading stays on the account, at the floor and marked lapsed, not dropped", () => {
|
|
121
|
+
const NOW = 1_700_000_000_000;
|
|
122
|
+
const HOUR = 3_600_000;
|
|
123
|
+
// The case that lost the five-hour row: re-reads failing for hours, the weekly pools still current.
|
|
124
|
+
const stale: AccountUsage = {
|
|
125
|
+
measuredAt: NOW - 8 * HOUR,
|
|
126
|
+
windows: [
|
|
127
|
+
window({ kind: "five_hour", utilization: 63, resetsAt: (NOW - 5 * HOUR) / 1_000, rounded: true }),
|
|
128
|
+
window({ kind: "seven_day", utilization: 42, resetsAt: NOW / 1_000 + 100 * 3_600 }),
|
|
129
|
+
window({ kind: "model:Fable", label: "Fable", utilization: 0, resetsAt: NOW / 1_000 + 100 * 3_600, gates: { models: ["Fable"] } }),
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
const current = currentWindows(stale, NOW);
|
|
133
|
+
expect(current.map((pool) => pool.kind)).toEqual(["five_hour", "seven_day", "model:Fable"]);
|
|
134
|
+
expect(current[0]).toEqual({ kind: "five_hour", utilization: 0, gates: "all", lapsedAt: (NOW - 5 * HOUR) / 1_000 });
|
|
135
|
+
expect(current.slice(1)).toEqual(stale.windows.slice(1));
|
|
136
|
+
// The floor binds nothing: the weekly pool is still the one that gates the next turn.
|
|
137
|
+
expect(bindingWindow({ ...stale, windows: current })?.kind).toBe("seven_day");
|
|
138
|
+
// Applying it again changes nothing, so a surface may run it over rows the daemon already ran it over.
|
|
139
|
+
expect(currentWindows({ ...stale, windows: current }, NOW)).toEqual(current);
|
|
140
|
+
|
|
141
|
+
// An idle pool published with no reset lapses at the end of its own length.
|
|
142
|
+
const idle: AccountUsage = {
|
|
143
|
+
measuredAt: NOW - 6 * HOUR,
|
|
144
|
+
windows: [window({ kind: "five_hour", utilization: 0 }), window({ kind: "seven_day", utilization: 5, resetsAt: NOW / 1_000 + 3_600 })],
|
|
145
|
+
};
|
|
146
|
+
expect(currentWindows(idle, NOW)[0]?.lapsedAt).toBe((NOW - HOUR) / 1_000);
|
|
147
|
+
|
|
148
|
+
// A reading with nothing current left says nothing about now: the account reads as unmeasured.
|
|
149
|
+
expect(currentWindows({ measuredAt: NOW - 8 * HOUR, windows: stale.windows.slice(0, 1) }, NOW)).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
119
152
|
// Serviceability: the one verdict on whether an account can serve a turn.
|
|
120
153
|
|
|
121
154
|
const at = (measuredAt: number, ...windows: UsageWindow[]): AccountUsage => ({ windows, measuredAt });
|
|
@@ -248,6 +281,31 @@ test("a provider's accounts are judged together: a standing refusal lands only o
|
|
|
248
281
|
expect(answered.get("a")).toEqual({ kind: "ready", room: 60 });
|
|
249
282
|
});
|
|
250
283
|
|
|
284
|
+
// The provider's last refusal moves on to whichever account says no next; an account's own refusal on file stays true of
|
|
285
|
+
// it. A pool Anthropic reads at a rounded 100 is only proven empty by that refusal.
|
|
286
|
+
test("each account answers to the newest refusal on file that still stands over it", () => {
|
|
287
|
+
const line = at(5_000, window({ kind: "seven_day", utilization: 100, rounded: true, resetsAt: 20 }));
|
|
288
|
+
// Read before its sign-in was refused, so nothing has answered that refusal yet.
|
|
289
|
+
const room = at(2_000, window({ kind: "seven_day", utilization: 30 }));
|
|
290
|
+
const own = refusal({ kind: "limit", account: "a", resetsAt: 20, at: 1_000 });
|
|
291
|
+
const elsewhere = refusal({ kind: "auth", account: "b", at: 3_000 });
|
|
292
|
+
const accounts = [
|
|
293
|
+
{ account: "a", usage: line },
|
|
294
|
+
{ account: "b", usage: room },
|
|
295
|
+
];
|
|
296
|
+
expect(Object.fromEntries(serviceStates(accounts, [elsewhere, own], undefined, 9_999))).toEqual({
|
|
297
|
+
a: { kind: "spent", reopensAt: 20 },
|
|
298
|
+
b: { kind: "blocked", fix: "reconnect", reason: "no." },
|
|
299
|
+
});
|
|
300
|
+
// Only the provider's last one: the first account is back at the line, no room but not proven spent.
|
|
301
|
+
expect(serviceStates(accounts, elsewhere, undefined, 9_999).get("a")).toEqual({ kind: "ready", room: 0 });
|
|
302
|
+
// Past the instant the provider named, its refusal is over and the reading speaks alone.
|
|
303
|
+
expect(serviceStates(accounts, [elsewhere, own], undefined, 20_000).get("a")).toEqual({ kind: "ready", room: 0 });
|
|
304
|
+
// A newer refusal on the same account outranks the older one.
|
|
305
|
+
const revoked = refusal({ kind: "auth", account: "a", at: 6_000 });
|
|
306
|
+
expect(serviceStates(accounts, [own, revoked], undefined, 9_999).get("a")).toEqual({ kind: "blocked", fix: "reconnect", reason: "no." });
|
|
307
|
+
});
|
|
308
|
+
|
|
251
309
|
test("an unnamed turn takes the most room, then an unmeasured account, then a spent one, and a blocked one only when that is all", () => {
|
|
252
310
|
const entry = (id: string, state: AccountState) => ({ id, state });
|
|
253
311
|
const blocked: AccountState = { kind: "blocked", fix: "admin", reason: "no seat" };
|
package/src/models/plan-pools.ts
CHANGED
|
@@ -146,6 +146,28 @@ export const windowLive = (window: UsageWindow, measuredAt: number, now: number)
|
|
|
146
146
|
return period === undefined || measuredAt + period.seconds * 1000 > now;
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
// A pool whose reading belongs to a window that has since closed: still the account's pool, at the floor any new window
|
|
150
|
+
// starts from, and marked with when the old window closed so a surface can say it has not been re-read since.
|
|
151
|
+
const lapsedWindow = (window: UsageWindow, measuredAt: number): UsageWindow => {
|
|
152
|
+
const { resetsAt, rounded: _rounded, utilization: _utilization, ...pool } = window;
|
|
153
|
+
// Only reached for a window windowLive retired, so with no published reset its length is known.
|
|
154
|
+
const closedAt = resetsAt ?? Math.floor((measuredAt + (windowPeriod(window)?.seconds ?? 0) * 1000) / 1000);
|
|
155
|
+
return { ...pool, utilization: 0, lapsedAt: closedAt };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// What a reading says about each of the account's pools now. A window past its reset is kept rather than dropped, as a
|
|
159
|
+
// lapsed pool (lapsedWindow): dropping it made a reading whose re-reads were failing lose its five-hour row within hours
|
|
160
|
+
// while the weekly rows stayed, a partial card that reads as complete. The floor of 0 is what dropping it already meant
|
|
161
|
+
// to every verdict (an absent pool binds nothing). A reading with no window still current says nothing about now and
|
|
162
|
+
// answers none, so the account reads as unmeasured. Idempotent: a pool already lapsed stays as it is.
|
|
163
|
+
export const currentWindows = (usage: AccountUsage, now: number): UsageWindow[] => {
|
|
164
|
+
const live = (window: UsageWindow): boolean => window.lapsedAt === undefined && windowLive(window, usage.measuredAt, now);
|
|
165
|
+
if (!usage.windows.some(live)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
return usage.windows.map((window) => (window.lapsedAt !== undefined || live(window) ? window : lapsedWindow(window, usage.measuredAt)));
|
|
169
|
+
};
|
|
170
|
+
|
|
149
171
|
// Whether an account can serve a turn: the one rule every picker and surface reads (AccountState). The daemon runs it and
|
|
150
172
|
// publishes the verdict on each account row; the editor runs it only on a row from a daemon older than that field.
|
|
151
173
|
|
|
@@ -283,19 +305,28 @@ export const serviceState = (facts: ServiceFacts, refusal?: ProviderRefusal, mod
|
|
|
283
305
|
return refusal?.kind === "limit" ? refusedPool(facts.usage, refusal, model) : headroomState(facts.usage, model);
|
|
284
306
|
};
|
|
285
307
|
|
|
286
|
-
/**
|
|
308
|
+
/** The refusals a provider's accounts are judged against: its last one, or every one on file (each account's own beside it). */
|
|
309
|
+
export type RefusalsOnFile = ProviderRefusal | readonly ProviderRefusal[] | undefined;
|
|
310
|
+
|
|
311
|
+
const refusalList = (refusals: RefusalsOnFile): readonly ProviderRefusal[] =>
|
|
312
|
+
refusals === undefined ? [] : "kind" in refusals ? [refusals] : refusals;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Every account of one provider, judged together, since a refusal naming no account is answered by any of them. Each
|
|
316
|
+
* account answers to the newest refusal that still stands over it: the provider's last one moves on to whichever account
|
|
317
|
+
* said no next, and an account's own refusal on file stays true of it until something answers it.
|
|
318
|
+
*/
|
|
287
319
|
export const serviceStates = (
|
|
288
320
|
accounts: readonly ServiceFacts[],
|
|
289
|
-
|
|
321
|
+
refusals: RefusalsOnFile,
|
|
290
322
|
model?: ModelRef,
|
|
291
323
|
now: number = Date.now(),
|
|
292
324
|
): ReadonlyMap<string, AccountState> => {
|
|
293
|
-
const standing =
|
|
325
|
+
const standing = refusalList(refusals)
|
|
326
|
+
.filter((refusal) => refusalVerdict(refusal, accounts, now) === "standing")
|
|
327
|
+
.toSorted((left, right) => right.at - left.at);
|
|
294
328
|
return new Map(
|
|
295
|
-
accounts.map((facts) => [
|
|
296
|
-
facts.account,
|
|
297
|
-
serviceState(facts, standing !== undefined && (standing.account ?? facts.account) === facts.account ? standing : undefined, model, now),
|
|
298
|
-
]),
|
|
329
|
+
accounts.map((facts) => [facts.account, serviceState(facts, standing.find((refusal) => (refusal.account ?? facts.account) === facts.account), model, now)]),
|
|
299
330
|
);
|
|
300
331
|
};
|
|
301
332
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderBrand } from "@intentic/constants";
|
|
2
|
-
import { type AgentCapabilities, CLAUDE_CODE, CODEX, CURSOR, OPENCODE, OPENCODE_GEMINI } from "./agent-runtimes.js";
|
|
2
|
+
import { type AgentCapabilities, CLAUDE_CODE, CODEX, CURSOR, DEVIN, OPENCODE, OPENCODE_GEMINI } from "./agent-runtimes.js";
|
|
3
3
|
|
|
4
4
|
// Every fact about a provider more than one surface needs, one row each, replacing lists that drifted out of sync.
|
|
5
5
|
// `access` (cost) and `auth` (connection) stay separate: conflating them mis-describes Z.ai, a prepaid plan spent
|
|
@@ -150,6 +150,24 @@ export const PROVIDER_SPECS = [
|
|
|
150
150
|
// Cursor ignores harness too: there is no route to it but its own SDK.
|
|
151
151
|
runtimes: { native: CURSOR, claudeCode: CURSOR },
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
// Cognition's Devin through its own CLI (`devin acp`); labelled for the account, since it vends many vendors'
|
|
155
|
+
// models (SWE, Claude, GPT, Gemini…) under one Devin plan.
|
|
156
|
+
id: "devin",
|
|
157
|
+
label: "Devin",
|
|
158
|
+
vendor: "Devin",
|
|
159
|
+
accountLabel: "Devin",
|
|
160
|
+
destination: "Devin",
|
|
161
|
+
brand: "devin",
|
|
162
|
+
// Free is a real tier here: a free Devin account runs turns, on the smaller list of models its plan offers.
|
|
163
|
+
access: { kind: "free", requirement: "Devin account", runs: "Devin's agent on the models your plan offers" },
|
|
164
|
+
// The daemon drives the CLI's own sign-in and holds the key it hands back, one per account.
|
|
165
|
+
auth: { kind: "oauth" },
|
|
166
|
+
// Devin publishes its quota only on its own pages and in its CLI's interactive view, nothing a key can read.
|
|
167
|
+
planLimits: false,
|
|
168
|
+
// The CLI is the only door, so harness is not a choice: both axes are Devin's own runtime.
|
|
169
|
+
runtimes: { native: DEVIN, claudeCode: DEVIN },
|
|
170
|
+
},
|
|
153
171
|
// The two minted providers need no new runtime or translator hop: each has its own Anthropic Messages endpoint.
|
|
154
172
|
{
|
|
155
173
|
id: "meta",
|
|
@@ -223,7 +241,7 @@ export const PROVIDER_SPECS = [
|
|
|
223
241
|
type Spec = (typeof PROVIDER_SPECS)[number];
|
|
224
242
|
|
|
225
243
|
// The agent runtimes the daemon can serve; a native id is reserved, `endpoint/<id>` or any other value names an
|
|
226
|
-
// installed capability instead. Derived from the table and a union of
|
|
244
|
+
// installed capability instead. Derived from the table and a union of its ids, so the wire vocabulary can't drift.
|
|
227
245
|
export type NativeProvider = Spec["id"];
|
|
228
246
|
export const NATIVE_PROVIDERS: readonly NativeProvider[] = PROVIDER_SPECS.map((spec) => spec.id);
|
|
229
247
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HISTORY_ROOT, WORKSPACE_ROOT } from "@intentic/constants";
|
|
2
2
|
import { type CommandClass, CommandClassSchema, type CommandLocus } from "../schemas/agent.js";
|
|
3
|
+
import { SECRET_REFERENCE } from "../schemas/needs.js";
|
|
3
4
|
import { inertRegions, isLive } from "../text/shell-regions.js";
|
|
4
5
|
|
|
5
6
|
// Classifies a shell command, shared by the sandbox gate and the machine agent's shell tool so they can't drift on what
|
|
@@ -72,7 +73,7 @@ const GIT_DESTRUCTIVE = [
|
|
|
72
73
|
const GIT_BRANCH_SWITCH = [/\bgit\s+switch\b/, /\bgit\s+checkout\b(?![^|;&]*\s--\s)(?![^|;&]*\s\.(?:\s|$))/];
|
|
73
74
|
|
|
74
75
|
// The credential is IN the command, not in a file; there is nothing for a filesystem to fact-check here.
|
|
75
|
-
const SECRET_REFERENCES = [
|
|
76
|
+
const SECRET_REFERENCES = [SECRET_REFERENCE];
|
|
76
77
|
|
|
77
78
|
// A path that usually holds a credential, a guess about a FILE, so every entry here is subject to holdsSecret.
|
|
78
79
|
// The directory in front of a filename, so a pattern spans the whole path, not just the file inside it.
|
|
@@ -404,6 +405,23 @@ const findAndXargsDeletes = (command: string): CommandSpan[] => [
|
|
|
404
405
|
...spansOf([XARGS_RM], command),
|
|
405
406
|
];
|
|
406
407
|
|
|
408
|
+
// The same tree delete in Windows' own shells, which is what a device's run_command runs there: PowerShell's
|
|
409
|
+
// Remove-Item (and its aliases) with -Recurse, which it answers without asking, and cmd's rd/rmdir /s and del /s. Only
|
|
410
|
+
// PowerShell's own names are read with a bare -r (any prefix of -Recurse, which PowerShell accepts), since `rm -r` is
|
|
411
|
+
// also POSIX rm, whose bare -r asks before anything write-protected and is left to the rm rule above. And .NET's
|
|
412
|
+
// Directory.Delete(path, true), the same delete from a script.
|
|
413
|
+
const POWERSHELL_RECURSE = String.raw`\s-r(?:e(?:c(?:u(?:r(?:s(?:e)?)?)?)?)?)?(?::\$true)?(?![\w-])`;
|
|
414
|
+
const WINDOWS_RECURSIVE_DELETES = [
|
|
415
|
+
new RegExp(String.raw`(?<![\w-])(?:Remove-Item|ri|rd|rmdir|del|erase)\b[^|;&\n]*?${POWERSHELL_RECURSE}`, "i"),
|
|
416
|
+
new RegExp(String.raw`(?<![\w-])rm\b[^|;&\n]*?\s-recurse(?![\w-])`, "i"),
|
|
417
|
+
/(?<![\w-])(?:rd|rmdir|del|erase)\b[^|;&\n]*?\s\/s\b/i,
|
|
418
|
+
/\[(?:System\.)?IO\.Directory\]::Delete\s*\([^)]*,\s*\$true\s*\)/i,
|
|
419
|
+
];
|
|
420
|
+
const windowsDeletes = (command: string): CommandSpan[] => spansOf(globally(WINDOWS_RECURSIVE_DELETES), command);
|
|
421
|
+
|
|
422
|
+
// A disk wiped by Windows' own tools: a volume formatted, a disk cleared, from PowerShell or cmd.
|
|
423
|
+
const WINDOWS_DISK_WIPES = [/(?<![\w-])(?:Format-Volume|Clear-Disk)\b/i, /(?<![\w-])format(?:\.com)?\s+[A-Za-z]:/i];
|
|
424
|
+
|
|
407
425
|
// What cuts a checkout off from its repository's history: deleting its `.git` (for a worktree, a one-line pointer
|
|
408
426
|
// whose loss strands every commit the checkout was reading), or `git init` over the folder it stands in, which makes
|
|
409
427
|
// a standalone repository sharing nothing with the one it came from. A `git init <new-dir>` for a new project is not
|
|
@@ -500,8 +518,17 @@ const MATCHES: Readonly<Record<CommandClass, (command: string, context: CommandC
|
|
|
500
518
|
"git.destructive": (command) => [...spansOf(GIT_DESTRUCTIVE_G, command), ...checkoutUnlinks(command)],
|
|
501
519
|
// The only class that can answer "not here": in its own copy a conversation's checkout is its own to move.
|
|
502
520
|
"git.branch-switch": (command, context) => (context.ownCheckout === true ? [] : spansOf(GIT_BRANCH_SWITCH_G, command)),
|
|
503
|
-
"files.destructive": (command) => [
|
|
504
|
-
|
|
521
|
+
"files.destructive": (command) => [
|
|
522
|
+
...recursiveForceRms(command),
|
|
523
|
+
...recursiveDeletes(command),
|
|
524
|
+
...findAndXargsDeletes(command),
|
|
525
|
+
...windowsDeletes(command),
|
|
526
|
+
],
|
|
527
|
+
"system.destructive": (command, context) => [
|
|
528
|
+
...spansOf(BLOCK_DEVICE_G, command),
|
|
529
|
+
...spansOf(globally(WINDOWS_DISK_WIPES), command),
|
|
530
|
+
...rootDeletes(command, context.locus),
|
|
531
|
+
],
|
|
505
532
|
"container.state": (command) => spansOf(CONTAINER_STATE_G, command),
|
|
506
533
|
"secrets.access": credentialReads,
|
|
507
534
|
"package.publish": (command) => spansOf(PACKAGE_PUBLISH_G, command),
|
|
@@ -565,6 +592,9 @@ export const COMMAND_CLASS_PATTERNS: Readonly<Record<CommandClass, readonly Comm
|
|
|
565
592
|
{ code: "rimraf(<path>)" },
|
|
566
593
|
{ code: "find <path> -delete", qualifier: "also -exec rm and -execdir rm" },
|
|
567
594
|
{ code: "xargs rm" },
|
|
595
|
+
{ code: "Remove-Item -Recurse <path>", qualifier: "also its aliases ri, rd, rmdir, del, erase, and rm -Recurse" },
|
|
596
|
+
{ code: "rd /s <path>", qualifier: "also rmdir /s and del /s" },
|
|
597
|
+
{ code: "[IO.Directory]::Delete(<path>, $true)" },
|
|
568
598
|
],
|
|
569
599
|
"system.destructive": [
|
|
570
600
|
{ code: "mkfs" },
|
|
@@ -574,6 +604,7 @@ export const COMMAND_CLASS_PATTERNS: Readonly<Record<CommandClass, readonly Comm
|
|
|
574
604
|
{ code: "dd of=/dev/…" },
|
|
575
605
|
{ code: "shred /dev/…" },
|
|
576
606
|
{ code: "> /dev/sda" },
|
|
607
|
+
{ code: "Format-Volume", qualifier: "also Clear-Disk, and format C:" },
|
|
577
608
|
{ code: "rm -rf /", qualifier: "also find / -delete; only when the target is a root, listed below" },
|
|
578
609
|
],
|
|
579
610
|
"container.state": [
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeRegExp } from "@intentic/base/escape";
|
|
2
|
+
|
|
1
3
|
// Whether a file's text actually holds a credential, the fact command-classes.ts's path table can only guess at from a
|
|
2
4
|
// name. Pure over the text (no fs, no config), so it stays testable and works where there's no filesystem. Biased
|
|
3
5
|
// toward yes: a false positive costs one card, a false negative un-gates a real credential read.
|
|
@@ -25,24 +27,62 @@ const STRUCTURAL_SHAPES = [
|
|
|
25
27
|
/\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:(?!\*+@)[^\s/@]{3,}@/i,
|
|
26
28
|
];
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
// An issuer's literal prefix and the regex source of the body it mints after it.
|
|
31
|
+
export interface CredentialIssuer {
|
|
32
|
+
readonly prefix: string;
|
|
33
|
+
readonly body: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Credentials that name their own issuer. Kept as data, not regexes, so the terminal filter's copy
|
|
37
|
+
// (CREDENTIAL_ISSUERS in _sandbox/output-cleaners/src/cleaners.mjs, builtins only, so it cannot import this) can be
|
|
38
|
+
// compared entry by entry: the daemon's src/secrets/terminal-masking-parity.test.ts fails the moment the two part. Only
|
|
39
|
+
// the whole shape counts here, never the prefix alone: the outbox reads text nobody said holds a credential, where a
|
|
40
|
+
// bare `sk-` in prose is not one. The terminal side also takes the prefix alone beside a credential-shaped name, where
|
|
41
|
+
// the name already said so.
|
|
42
|
+
export const CREDENTIAL_ISSUERS: readonly CredentialIssuer[] = [
|
|
43
|
+
{ prefix: "npm_", body: "[A-Za-z0-9]{30,}" },
|
|
44
|
+
{ prefix: "ghp_", body: "[A-Za-z0-9]{20,}" },
|
|
45
|
+
{ prefix: "gho_", body: "[A-Za-z0-9]{20,}" },
|
|
46
|
+
{ prefix: "ghu_", body: "[A-Za-z0-9]{20,}" },
|
|
47
|
+
{ prefix: "ghs_", body: "[A-Za-z0-9]{20,}" },
|
|
48
|
+
{ prefix: "ghr_", body: "[A-Za-z0-9]{20,}" },
|
|
49
|
+
{ prefix: "github_pat_", body: "[A-Za-z0-9_]{50,}" },
|
|
50
|
+
{ prefix: "glpat-", body: "[A-Za-z0-9_-]{16,}" },
|
|
51
|
+
{ prefix: "xoxa-", body: "[A-Za-z0-9-]{10,}" },
|
|
52
|
+
{ prefix: "xoxb-", body: "[A-Za-z0-9-]{10,}" },
|
|
53
|
+
{ prefix: "xoxo-", body: "[A-Za-z0-9-]{10,}" },
|
|
54
|
+
{ prefix: "xoxp-", body: "[A-Za-z0-9-]{10,}" },
|
|
55
|
+
{ prefix: "xoxr-", body: "[A-Za-z0-9-]{10,}" },
|
|
56
|
+
{ prefix: "xoxs-", body: "[A-Za-z0-9-]{10,}" },
|
|
57
|
+
{ prefix: "sk-", body: "[A-Za-z0-9_-]{20,}" },
|
|
58
|
+
{ prefix: "pk-", body: "[A-Za-z0-9_-]{20,}" },
|
|
59
|
+
{ prefix: "rk-", body: "[A-Za-z0-9_-]{20,}" },
|
|
60
|
+
{ prefix: "sk_live_", body: "[A-Za-z0-9]{16,}" },
|
|
61
|
+
{ prefix: "rk_live_", body: "[A-Za-z0-9]{16,}" },
|
|
62
|
+
{ prefix: "AKIA", body: String.raw`[0-9A-Z]{16}\b` },
|
|
63
|
+
{ prefix: "ASIA", body: String.raw`[0-9A-Z]{16}\b` },
|
|
64
|
+
{ prefix: "AIza", body: String.raw`[0-9A-Za-z_-]{35}\b` },
|
|
65
|
+
{ prefix: "ya29.", body: "[0-9A-Za-z_-]{20,}" },
|
|
66
|
+
{ prefix: "hf_", body: "[A-Za-z0-9]{30,}" },
|
|
67
|
+
{ prefix: "dop_v1_", body: "[a-f0-9]{60,}" },
|
|
68
|
+
{ prefix: "shpat_", body: "[a-fA-F0-9]{32,}" },
|
|
69
|
+
// SendGrid: a key id and its secret, dot-separated.
|
|
70
|
+
{ prefix: "SG.", body: String.raw`[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}` },
|
|
71
|
+
// A JWT: three base64url segments, the first two of which decode from `{"` and so always begin `eyJ` and `ey`.
|
|
72
|
+
{ prefix: "eyJ", body: String.raw`[A-Za-z0-9_-]{9,}\.ey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]*` },
|
|
43
73
|
];
|
|
74
|
+
export const CREDENTIAL_ISSUER_PREFIXES: readonly string[] = CREDENTIAL_ISSUERS.map(({ prefix }) => prefix);
|
|
75
|
+
|
|
76
|
+
// Starts where no word or kebab-case word runs into it: `task-sk-runner-config-file` is a name, not a key. The other
|
|
77
|
+
// side builds its patterns the same way.
|
|
78
|
+
const ISSUED_TOKENS = CREDENTIAL_ISSUERS.map(({ prefix, body }) => new RegExp(String.raw`(?<![\w-])${escapeRegExp(prefix)}${body}`));
|
|
79
|
+
|
|
80
|
+
// An opaque bearer credential, which names no issuer: the scheme word, then an RFC 6750 token holding a letter and a
|
|
81
|
+
// digit and longer than prose puts after "Bearer" (`Bearer <token>`, `Bearer YOUR_TOKEN`, an already-masked `***`).
|
|
82
|
+
const BEARER_VALUE = String.raw`(?=[\w.~+/-]*\d)(?=[\w.~+/-]*[A-Za-z])[\w.~+/-]{16,}=*`;
|
|
83
|
+
const BEARER_TOKEN = new RegExp(String.raw`\bBearer[ \t]+${BEARER_VALUE}`, "i");
|
|
44
84
|
|
|
45
|
-
const TOKEN_SHAPES = [...STRUCTURAL_SHAPES, ...ISSUED_TOKENS];
|
|
85
|
+
const TOKEN_SHAPES = [...STRUCTURAL_SHAPES, BEARER_TOKEN, ...ISSUED_TOKENS];
|
|
46
86
|
|
|
47
87
|
// Deferred references, stripped first: unstripped, each reads as a credential twice over.
|
|
48
88
|
const DEFERRED_VALUE = /\{\{[^}\n]*\}\}|\$\{[^}\n]*\}/g;
|
|
@@ -91,6 +131,7 @@ const PRIVATE_KEY_BLOCK = new RegExp(
|
|
|
91
131
|
);
|
|
92
132
|
const PUTTY_KEY = new RegExp(String.raw`(PuTTY-User-Key-File-\d:)${MASKED_NEXT}[^\n\\]*${ARMOR_LINES}`, "g");
|
|
93
133
|
const URL_PASSWORD = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:)(?!\*+@)[^\s/@]{3,}@/gi;
|
|
134
|
+
const BEARER_TOKEN_G = new RegExp(String.raw`(\bBearer[ \t]+)${BEARER_VALUE}`, "gi");
|
|
94
135
|
|
|
95
136
|
// The truncated opener left once the bare-value arm stops at its first `}`, anchored at the start.
|
|
96
137
|
const DEFERRED_OPENER = /^(?:\{\{|\$)/;
|
|
@@ -106,6 +147,7 @@ export const maskCredentialMaterial = (text: string): string =>
|
|
|
106
147
|
.replace(PUTTY_KEY, `$1 ${MASK}`),
|
|
107
148
|
)
|
|
108
149
|
.replace(URL_PASSWORD, `$1${MASK}@`)
|
|
150
|
+
.replace(BEARER_TOKEN_G, `$1${MASK}`)
|
|
109
151
|
// Masks just the value, leaving key/separator/quoting readable; skips mirror the detector's own.
|
|
110
152
|
.replace(CREDENTIAL_ASSIGNMENT_G, (match, ...groups: unknown[]) => {
|
|
111
153
|
const value = (groups[0] ?? groups[1] ?? groups[2] ?? "") as string;
|
|
@@ -58,3 +58,138 @@ export const includeGlobs = (include: string | undefined): IncludeGlobs => {
|
|
|
58
58
|
notGlobs: segments.filter((segment) => segment.startsWith(`!`)).flatMap((segment) => expand(segment.slice(1))),
|
|
59
59
|
};
|
|
60
60
|
};
|
|
61
|
+
|
|
62
|
+
// ONE GLOB MATCHER, with Bun.Glob's reading of every construct, so the daemon, the desktop folder server (which runs
|
|
63
|
+
// Bun.Glob itself) and the demo admit the same files for the same pattern. Before this the daemon's copy took
|
|
64
|
+
// `[!t]` as a set of the characters `!` and `t`, where Bun.Glob reads "anything but t".
|
|
65
|
+
// - `*` is any run within one path segment, `?` one character of it
|
|
66
|
+
// - `**` as a whole segment is any number of segments, none included (`**/x` matches `x`); inside a segment it is `*`
|
|
67
|
+
// - `[abc]`, `[a-z]`, and `[!abc]` or `[^abc]` for "none of these"; a `]` first in the set is literal
|
|
68
|
+
// - `{a,b}` is either, nested groups and globs inside included; `\` makes the next character literal
|
|
69
|
+
// - an unclosed `[` or `{` is literal, where Bun.Glob matches nothing: a half-typed pattern then still finds its text
|
|
70
|
+
const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\/]/;
|
|
71
|
+
|
|
72
|
+
const literal = (char: string): string => (REGEXP_SPECIAL.test(char) ? `\\${char}` : char);
|
|
73
|
+
|
|
74
|
+
// The index of the `]` closing the set opened at `open`, or -1.
|
|
75
|
+
const setEnd = (glob: string, open: number): number => {
|
|
76
|
+
let index = open + 1;
|
|
77
|
+
if (glob[index] === `!` || glob[index] === `^`) {
|
|
78
|
+
index += 1;
|
|
79
|
+
}
|
|
80
|
+
// A `]` straight after the opening (or its negation) is a member, not the close.
|
|
81
|
+
if (glob[index] === `]`) {
|
|
82
|
+
index += 1;
|
|
83
|
+
}
|
|
84
|
+
for (; index < glob.length; index += 1) {
|
|
85
|
+
if (glob[index] === `\\`) {
|
|
86
|
+
index += 1;
|
|
87
|
+
} else if (glob[index] === `]`) {
|
|
88
|
+
return index;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return -1;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const setSource = (body: string): string => {
|
|
95
|
+
const negated = body.startsWith(`!`) || body.startsWith(`^`);
|
|
96
|
+
const members = (negated ? body.slice(1) : body).replaceAll(/\\(.)/g, `$1`).replaceAll(/[\\\]^[]/g, (char) => `\\${char}`);
|
|
97
|
+
return `[${negated ? `^` : ``}${members}]`;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// The index of the `}` closing the group opened at `open`, and where its top-level commas fall; undefined if unclosed.
|
|
101
|
+
const groupOf = (glob: string, open: number): { readonly end: number; readonly commas: readonly number[] } | undefined => {
|
|
102
|
+
const commas: number[] = [];
|
|
103
|
+
let depth = 0;
|
|
104
|
+
for (let index = open; index < glob.length; index += 1) {
|
|
105
|
+
const char = glob[index];
|
|
106
|
+
if (char === `\\`) {
|
|
107
|
+
index += 1;
|
|
108
|
+
} else if (char === `[`) {
|
|
109
|
+
const end = setEnd(glob, index);
|
|
110
|
+
index = end === -1 ? index : end;
|
|
111
|
+
} else if (char === `{`) {
|
|
112
|
+
depth += 1;
|
|
113
|
+
} else if (char === `}`) {
|
|
114
|
+
depth -= 1;
|
|
115
|
+
if (depth === 0) {
|
|
116
|
+
return { end: index, commas };
|
|
117
|
+
}
|
|
118
|
+
} else if (char === `,` && depth === 1) {
|
|
119
|
+
commas.push(index);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// `**` filling a whole segment: the source it stands for and how many characters of the glob it consumed.
|
|
126
|
+
const globstar = (glob: string, index: number, segmentStart: boolean): { readonly source: string; readonly length: number } | undefined => {
|
|
127
|
+
if (!segmentStart || glob[index + 1] !== `*`) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const after = glob[index + 2];
|
|
131
|
+
if (after === `/`) {
|
|
132
|
+
return { source: `(?:[^/]*/)*`, length: 3 };
|
|
133
|
+
}
|
|
134
|
+
return after === undefined ? { source: `.*`, length: 2 } : undefined;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const compileGlob = (glob: string, segmentStart: boolean): string => {
|
|
138
|
+
let source = ``;
|
|
139
|
+
let atStart = segmentStart;
|
|
140
|
+
for (let index = 0; index < glob.length; index += 1) {
|
|
141
|
+
const char = glob[index]!;
|
|
142
|
+
const star = char === `*` ? globstar(glob, index, atStart) : undefined;
|
|
143
|
+
if (star !== undefined) {
|
|
144
|
+
source += star.source;
|
|
145
|
+
index += star.length - 1;
|
|
146
|
+
atStart = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (char === `*`) {
|
|
150
|
+
// A run of stars inside a segment is one star.
|
|
151
|
+
while (glob[index + 1] === `*`) {
|
|
152
|
+
index += 1;
|
|
153
|
+
}
|
|
154
|
+
source += `[^/]*`;
|
|
155
|
+
} else if (char === `?`) {
|
|
156
|
+
source += `[^/]`;
|
|
157
|
+
} else if (char === `\\` && index + 1 < glob.length) {
|
|
158
|
+
index += 1;
|
|
159
|
+
source += literal(glob[index]!);
|
|
160
|
+
} else if (char === `[` && setEnd(glob, index) !== -1) {
|
|
161
|
+
const end = setEnd(glob, index);
|
|
162
|
+
source += setSource(glob.slice(index + 1, end));
|
|
163
|
+
index = end;
|
|
164
|
+
} else if (char === `{` && groupOf(glob, index) !== undefined) {
|
|
165
|
+
const group = groupOf(glob, index)!;
|
|
166
|
+
const bounds = [index, ...group.commas, group.end];
|
|
167
|
+
const alternatives = bounds.slice(1).map((end, at) => compileGlob(glob.slice(bounds[at]! + 1, end), atStart));
|
|
168
|
+
source += `(?:${alternatives.join(`|`)})`;
|
|
169
|
+
index = group.end;
|
|
170
|
+
} else {
|
|
171
|
+
source += literal(char);
|
|
172
|
+
}
|
|
173
|
+
atStart = char === `/`;
|
|
174
|
+
}
|
|
175
|
+
return source;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// The whole path must match: `*.ts` is a name at the root, not a suffix anywhere.
|
|
179
|
+
export const globRegExp = (glob: string): RegExp => new RegExp(`^${compileGlob(glob, true)}$`);
|
|
180
|
+
|
|
181
|
+
// A matcher for slash-separated relative paths, compiled once.
|
|
182
|
+
export const globMatcher = (glob: string): ((path: string) => boolean) => {
|
|
183
|
+
const pattern = globRegExp(glob);
|
|
184
|
+
return (path) => pattern.test(path);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// The search box's whole second field as one test over a relative path: admitted by any include (or by none being
|
|
188
|
+
// typed), and by no exclude. An anchored include's `./` is the root, so it is dropped before matching.
|
|
189
|
+
export const includeMatcher = (include: string | undefined): ((path: string) => boolean) => {
|
|
190
|
+
const { globs, notGlobs } = includeGlobs(include);
|
|
191
|
+
const rooted = (glob: string): string => glob.replace(/^\.\//, ``);
|
|
192
|
+
const admits = globs.map((glob) => globMatcher(rooted(glob)));
|
|
193
|
+
const refuses = notGlobs.map((glob) => globMatcher(rooted(glob)));
|
|
194
|
+
return (path) => (admits.length === 0 || admits.some((test) => test(path))) && !refuses.some((test) => test(path));
|
|
195
|
+
};
|
|
@@ -42,6 +42,13 @@ export const normalizeHost = (raw: string): string | undefined => {
|
|
|
42
42
|
export const hostMatches = (pattern: string, host: string): boolean =>
|
|
43
43
|
pattern.startsWith(WILDCARD) ? host.endsWith(pattern.slice(1)) : host === pattern;
|
|
44
44
|
|
|
45
|
+
// Whether one entry of a domain list covers a host: a plain entry names a domain and every host under it at any depth
|
|
46
|
+
// (`example.com` covers `example.com` and `api.example.com`, never `notexample.com`), a wildcard only the hosts under it.
|
|
47
|
+
// Both sides are expected normalized. The wallet's allow and deny lists read their entries this way; a secret's host
|
|
48
|
+
// guard reads its own exactly (hostMatches).
|
|
49
|
+
export const hostCovers = (entry: string, host: string): boolean =>
|
|
50
|
+
entry.startsWith(WILDCARD) ? hostMatches(entry, host) : host === entry || host.endsWith(`.${entry}`);
|
|
51
|
+
|
|
45
52
|
export const hostAllowed = (patterns: readonly string[], host: string): boolean => patterns.some((pattern) => hostMatches(pattern, host));
|
|
46
53
|
|
|
47
54
|
// Whether every host `pattern` matches is already matched by `patterns`: an exact host by any pattern that matches it, a
|
|
@@ -656,3 +656,38 @@ describe("git.branch-switch", () => {
|
|
|
656
656
|
expect(marked("cd repo && git switch main && pnpm build", "git.branch-switch")).toEqual(["git switch"]);
|
|
657
657
|
});
|
|
658
658
|
});
|
|
659
|
+
|
|
660
|
+
describe("Windows' own shells", () => {
|
|
661
|
+
test("PowerShell's and cmd's recursive deletes are files.destructive, the way a device's run_command meets them", () => {
|
|
662
|
+
for (const command of [
|
|
663
|
+
"Remove-Item -Recurse -Force C:\\Users\\me\\thesis",
|
|
664
|
+
"Remove-Item C:\\x -Recurse",
|
|
665
|
+
"Remove-Item -Recurse:$true x",
|
|
666
|
+
"ri -r C:\\x",
|
|
667
|
+
"rm -Recurse x",
|
|
668
|
+
"rd /s /q C:\\x",
|
|
669
|
+
"rmdir /S C:\\x",
|
|
670
|
+
"del /s /q *.tmp",
|
|
671
|
+
"[System.IO.Directory]::Delete('C:\\x', $true)",
|
|
672
|
+
"powershell -Command 'Remove-Item -Recurse -Force C:\\x'",
|
|
673
|
+
"cmd /c 'rmdir /s /q C:\\x'",
|
|
674
|
+
]) {
|
|
675
|
+
expect(classify(command), command).toContain("files.destructive");
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("what only looks like one is not: a single file, a read that recurses, POSIX rm's asking -r", () => {
|
|
680
|
+
for (const command of ["Remove-Item C:\\x\\file.txt", "del file.txt", "Get-ChildItem -Recurse C:\\x", "Copy-Item -Recurse a b", "rmdir empty", "rm -r build", "git rm -r --cached x"]) {
|
|
681
|
+
expect(classify(command), command).not.toContain("files.destructive");
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("a volume formatted or a disk cleared is system.destructive", () => {
|
|
686
|
+
for (const command of ["Format-Volume -DriveLetter D", "Clear-Disk -Number 1 -RemoveData", "format D: /q"]) {
|
|
687
|
+
expect(classify(command), command).toContain("system.destructive");
|
|
688
|
+
}
|
|
689
|
+
for (const command of ["npm run format", "prettier --write . && echo format done", "dotnet format"]) {
|
|
690
|
+
expect(classify(command), command).not.toContain("system.destructive");
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
});
|
|
@@ -202,6 +202,26 @@ describe("a credential token in text that is not a credential file", () => {
|
|
|
202
202
|
expect(holdsCredentialToken("-----BEGIN PRIVATE KEY-----\nMIIEvQ")).toBe(true);
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
// The issuers the terminal filter already knew; the two tables are one list now (ISSUERS).
|
|
206
|
+
test.each([
|
|
207
|
+
["an opaque bearer", "curl -H 'Authorization: Bearer 8f3kQ2mZ9xLp4RtV7wNb' https://api.example.com", "curl -H 'Authorization: Bearer ***' https://api.example.com"],
|
|
208
|
+
// Prefixes split from bodies so push protection does not read these fixtures as live keys.
|
|
209
|
+
["a shopify admin token", `token ${"shpat_"}0123456789abcdef0123456789abcdef in use`, "token *** in use"],
|
|
210
|
+
["a sendgrid key", `key ${"SG."}aB3dE5fG7hJ9kL1mN3pQ5r.sT7uV9wX1yZ3aB5cD7eF9gH1jK3lM5nP7qR9sT1uV3w set`, "key *** set"],
|
|
211
|
+
["a google oauth token", "refreshed ya29.a0AfB_byC3dE5fG7hJ9kL1mN3pQ5 ok", "refreshed *** ok"],
|
|
212
|
+
["a public-prefixed key", "langfuse pk-lf-0a1b2c3d-4e5f-6a7b-8c9d now", "langfuse *** now"],
|
|
213
|
+
])("names itself and masks: %s", (_label, text, masked) => {
|
|
214
|
+
expect(holdsCredentialToken(text)).toBe(true);
|
|
215
|
+
expect(maskCredentialMaterial(text)).toBe(masked);
|
|
216
|
+
expect(holdsCredentialToken(masked)).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("a bearer in prose or a template is not one", () => {
|
|
220
|
+
for (const text of ["Authorization: Bearer <token>", "Authorization: Bearer YOUR_API_TOKEN_HERE", "Bearer ${TOKEN}", "the Bearer scheme"]) {
|
|
221
|
+
expect(holdsCredentialToken(text), text).toBe(false);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
205
225
|
test("is never a credential-named key beside a value", () => {
|
|
206
226
|
expect(holdsCredentialToken("const config={apiKey:process.env.API_KEY,password:hunter22x}")).toBe(false);
|
|
207
227
|
expect(holdsCredentialMaterial("const config={apiKey:process.env.API_KEY,password:hunter22x}")).toBe(true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { includeGlobs } from "../search-globs.js";
|
|
1
|
+
import { globMatcher, includeGlobs, includeMatcher } from "../search-globs.js";
|
|
2
2
|
|
|
3
3
|
test("an empty field scopes nothing", () => {
|
|
4
4
|
expect(includeGlobs(undefined)).toEqual({ globs: [], notGlobs: [] });
|
|
@@ -43,3 +43,48 @@ test("a leading ! excludes instead", () => {
|
|
|
43
43
|
// A lone "!" excludes nothing: expanded, it would have matched everything.
|
|
44
44
|
expect(includeGlobs(`!`)).toEqual({ globs: [], notGlobs: [] });
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
describe("globMatcher", () => {
|
|
48
|
+
// Every construct, held against Bun.Glob itself: the desktop folder server matches with Bun.Glob, so any
|
|
49
|
+
// disagreement here is two machines admitting different files for one pattern.
|
|
50
|
+
const patterns = [
|
|
51
|
+
`*.ts`, `**/*.ts`, `**/*.[!t]s`, `**/*.[^t]s`, `src/**`, `src/**/*.ts`, `a/**/b`, `a?c`, `[a-c]x`, `[!a]`, `[]]`,
|
|
52
|
+
`*.{ts,js}`, `{a,{b,c}}/*`, `a/{b,c/**}`, `{**/x,y}`, `a**b`, `**`, `**/`, `**/**/x`, `{,a}b`, `\\*x`, `**.ts`, `a/**b`,
|
|
53
|
+
];
|
|
54
|
+
const paths = [
|
|
55
|
+
`a.ts`, `a.js`, `a/b.ts`, `a/b.js`, `.hidden.ts`, `.x/y.ts`, `src`, `src/a.ts`, `src/a/b.ts`, `a/b`, `a/x/b`, `abc`, `a/c`,
|
|
56
|
+
`bx`, `dx`, `/`, `a`, `]`, `c/x`, `a/c/d`, `a/x`, `x`, `y`, `axxb`, `ax/xb`, `b`, `ab`, `*x`, `a/xb`, `a/c/xb`, `a/`, `a/b/`,
|
|
57
|
+
];
|
|
58
|
+
for (const pattern of patterns) {
|
|
59
|
+
test(`reads ${pattern} as Bun.Glob does`, () => {
|
|
60
|
+
const glob = new Bun.Glob(pattern);
|
|
61
|
+
const matches = globMatcher(pattern);
|
|
62
|
+
expect(paths.filter((path) => matches(path))).toEqual(paths.filter((path) => glob.match(path)));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test("[!t] means anything but t", () => {
|
|
67
|
+
const matches = globMatcher(`**/*.[!t]s`);
|
|
68
|
+
expect([matches(`src/a.js`), matches(`src/a.ts`)]).toEqual([true, false]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("an unclosed set or group is literal", () => {
|
|
72
|
+
expect([globMatcher(`[ab`)(`[ab`), globMatcher(`{a,b`)(`{a,b`)]).toEqual([true, true]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("includeMatcher", () => {
|
|
77
|
+
test("admits by any include and refuses by any exclude", () => {
|
|
78
|
+
const matches = includeMatcher(`src, !*.test.ts`);
|
|
79
|
+
expect([`src/a.ts`, `src/a.test.ts`, `lib/src/b.ts`, `docs/c.md`].map(matches)).toEqual([true, false, true, false]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("an anchored include matches from the root only", () => {
|
|
83
|
+
const matches = includeMatcher(`./src`);
|
|
84
|
+
expect([matches(`src/a.ts`), matches(`lib/src/a.ts`)]).toEqual([true, false]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("nothing typed admits every path", () => {
|
|
88
|
+
expect(includeMatcher(undefined)(`any/where.ts`)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|