@intentic/sandbox-contract 1.330.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 +15 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +38 -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 +106 -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 +74 -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 +20 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +8 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +47 -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 +4 -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 +327 -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 +32 -17
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +30 -26
- 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 +5 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +41 -13
- 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 +35 -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/environment.d.ts +30 -0
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +13 -0
- package/dist/schemas/environment.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 +34 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +13 -2
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +8 -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 +6 -1
- package/dist/schemas/settings-history.d.ts.map +1 -1
- package/dist/schemas/settings-history.js +25 -1
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +69 -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 +42 -49
- package/src/models/model-setups.test.ts +100 -0
- package/src/models/model-setups.ts +151 -0
- package/src/models/plan-pools.test.ts +85 -0
- package/src/models/plan-pools.ts +76 -17
- 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/environment.ts +25 -0
- 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 +23 -2
- package/src/schemas/providers/usage.ts +13 -7
- package/src/schemas/run-targets.ts +5 -2
- package/src/schemas/settings-history.ts +43 -1
- package/src/schemas/settings.test.ts +84 -6
- 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,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
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hostAllowed, hostMatches, normalizeHost, normalizeHostPattern, tightensHostGuard } from "../secret-hosts.js";
|
|
1
|
+
import { hostAllowed, hostCovers, hostMatches, normalizeHost, normalizeHostPattern, tightensHostGuard } from "../secret-hosts.js";
|
|
2
2
|
|
|
3
3
|
// The pattern a person writes against the host a use would reach. Every boundary a limit turns on is pinned by value:
|
|
4
4
|
// the apex a wildcard leaves out, the look-alike that only shares a suffix, and which edits take hosts away.
|
|
@@ -87,3 +87,19 @@ describe("tightensHostGuard", () => {
|
|
|
87
87
|
expect(tightensHostGuard(on("api.github.com"), on("*.api.github.com"))).toBe(false);
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
describe("hostCovers", () => {
|
|
92
|
+
it("reads a plain entry as a domain and every host under it, never a lookalike", () => {
|
|
93
|
+
expect(hostCovers("example.com", "example.com")).toBe(true);
|
|
94
|
+
expect(hostCovers("example.com", "api.example.com")).toBe(true);
|
|
95
|
+
expect(hostCovers("example.com", "a.b.example.com")).toBe(true);
|
|
96
|
+
expect(hostCovers("example.com", "notexample.com")).toBe(false);
|
|
97
|
+
expect(hostCovers("example.com", "example.com.evil.net")).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("reads a wildcard as the hosts under it only", () => {
|
|
101
|
+
expect(hostCovers("*.example.com", "api.example.com")).toBe(true);
|
|
102
|
+
expect(hostCovers("*.example.com", "example.com")).toBe(false);
|
|
103
|
+
expect(hostCovers("*.example.com", "notexample.com")).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -13,6 +13,8 @@ const TURNED_AWAY: ReadonlySet<string> = new Set([
|
|
|
13
13
|
"model-helper-only",
|
|
14
14
|
// The privacy shield refused an untrusted provider on a runtime it cannot read by content at all.
|
|
15
15
|
"privacy-unshielded",
|
|
16
|
+
// The turn carries a tool list its runtime cannot keep.
|
|
17
|
+
"tools-unenforced",
|
|
16
18
|
// The protected execution boundary refused admission, not an executed provider/runtime failure.
|
|
17
19
|
"agent-domain-refused",
|
|
18
20
|
// The privacy shield found personal data in instructions a runtime loads itself, where it could not mask them.
|
|
@@ -20,6 +20,10 @@ export const RunnerHelloSchema = z.object({
|
|
|
20
20
|
// Compared against what the parent runs to report parity (outdated → rebuild action); an outdated runner still runs
|
|
21
21
|
// turns rather than being refused.
|
|
22
22
|
image: z.string(),
|
|
23
|
+
// The image `image` was composed from (SANDBOX_BASE_IMAGE). Parity compares this, not `image`: an overlay build's
|
|
24
|
+
// tag names its own container (`intentic-sandbox-env-<slug>:<hash>`), so two sides on one base and one overlay
|
|
25
|
+
// never share it. Absent on an old image, which is then compared by `image` alone.
|
|
26
|
+
baseImage: z.string().optional(),
|
|
23
27
|
channel: z.string().optional(),
|
|
24
28
|
overlayHash: z.string().optional(),
|
|
25
29
|
// Runner's declared settings-only sandbox.toml (definition.ts), diffed against the parent's to itemize drift;
|
|
@@ -106,13 +110,22 @@ export type RunnerCredentialRefreshRequest = z.infer<typeof RunnerCredentialRefr
|
|
|
106
110
|
export const RunnerCredentialRefreshSchema = z.object({ accessToken: z.string().optional() });
|
|
107
111
|
export type RunnerCredentialRefresh = z.infer<typeof RunnerCredentialRefreshSchema>;
|
|
108
112
|
|
|
109
|
-
// What a runner is, hardware-wise: what the placement picker shows and
|
|
113
|
+
// What a runner is, hardware-wise, and how full it is right now: what the placement picker shows and the fleet advice
|
|
114
|
+
// (runners/fleet-advice.ts) weighs. Re-asked on demand rather than only at connect, so the live half stays live.
|
|
110
115
|
export const RunnerFactsSchema = z.object({
|
|
111
116
|
cpus: z.number().int().positive(),
|
|
112
117
|
memoryMb: z.number().int().positive(),
|
|
113
118
|
freeDiskMb: z.number().int().nonnegative(),
|
|
114
119
|
// 0..1 over the last minute; a coarse busy signal for a picker, not a metrics feed.
|
|
115
120
|
load: z.number().nonnegative(),
|
|
121
|
+
// The runner's own resource budget, the figures its turn door admits by: what is free under its memory limit, and
|
|
122
|
+
// that limit. Absent from a runner that predates them, which is then sized by `memoryMb` alone.
|
|
123
|
+
memoryFreeMb: z.number().int().nonnegative().optional(),
|
|
124
|
+
memoryLimitMb: z.number().int().positive().optional(),
|
|
125
|
+
// Pressure stall over the last ten seconds, in percent: anything waiting for a CPU, everything waiting on memory.
|
|
126
|
+
// Absent where the kernel does not report it.
|
|
127
|
+
cpuPressure: z.number().nonnegative().optional(),
|
|
128
|
+
memoryStall: z.number().nonnegative().optional(),
|
|
116
129
|
});
|
|
117
130
|
export type RunnerFacts = z.infer<typeof RunnerFactsSchema>;
|
|
118
131
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { redeemWhileWarming, type RedeemRetry } from "./pairing-redeem.js";
|
|
2
|
+
|
|
3
|
+
/* The redeem loop both pairing peers run, over a sandbox the test plays one answer at a time. */
|
|
4
|
+
|
|
5
|
+
const scripted = (answers: readonly (number | "offline" | "hang")[]) => {
|
|
6
|
+
const tries: number[] = [];
|
|
7
|
+
const send = async (attempt: number, signal: AbortSignal | undefined): Promise<Response> => {
|
|
8
|
+
tries.push(attempt);
|
|
9
|
+
const next = answers[attempt - 1] ?? answers.at(-1) ?? "offline";
|
|
10
|
+
if (next === "offline") {
|
|
11
|
+
throw new TypeError("fetch failed");
|
|
12
|
+
}
|
|
13
|
+
if (next === "hang") {
|
|
14
|
+
return new Promise<Response>((_resolve, reject) => signal?.addEventListener("abort", () => reject(signal.reason)));
|
|
15
|
+
}
|
|
16
|
+
return new Response(`status ${next}`, { status: next });
|
|
17
|
+
};
|
|
18
|
+
return { tries, send };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const statusOf = (outcome: Awaited<ReturnType<typeof redeemWhileWarming>>): number | undefined =>
|
|
22
|
+
outcome.kind === "answered" ? outcome.response.status : undefined;
|
|
23
|
+
|
|
24
|
+
describe("redeemWhileWarming", () => {
|
|
25
|
+
it("answers the first reply below 500 without asking again", async () => {
|
|
26
|
+
const { tries, send } = scripted([200]);
|
|
27
|
+
expect(statusOf(await redeemWhileWarming(send, { attempts: 3, delayMs: 0 }))).toBe(200);
|
|
28
|
+
expect(tries).toEqual([1]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("asks again through a warming sandbox and an unreachable one, saying why each time", async () => {
|
|
32
|
+
const { tries, send } = scripted(["offline", 502, 200]);
|
|
33
|
+
const heard: { attempt: number; why: RedeemRetry["kind"] }[] = [];
|
|
34
|
+
const outcome = await redeemWhileWarming(send, { attempts: 5, delayMs: 0, onRetry: (attempt, why) => heard.push({ attempt, why: why.kind }) });
|
|
35
|
+
expect(statusOf(outcome)).toBe(200);
|
|
36
|
+
expect(tries).toEqual([1, 2, 3]);
|
|
37
|
+
expect(heard).toEqual([
|
|
38
|
+
{ attempt: 1, why: "unreachable" },
|
|
39
|
+
{ attempt: 2, why: "warming" },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("hands back a 401 at once: an expired code is an answer, not a blip", async () => {
|
|
44
|
+
const { tries, send } = scripted([401, 200]);
|
|
45
|
+
expect(statusOf(await redeemWhileWarming(send, { attempts: 5, delayMs: 0 }))).toBe(401);
|
|
46
|
+
expect(tries).toEqual([1]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("answers the last 5xx when the sandbox is still warming after every try", async () => {
|
|
50
|
+
const { tries, send } = scripted([503]);
|
|
51
|
+
const heard: number[] = [];
|
|
52
|
+
const outcome = await redeemWhileWarming(send, { attempts: 3, delayMs: 0, onRetry: (attempt) => heard.push(attempt) });
|
|
53
|
+
expect(outcome.kind).toBe("answered");
|
|
54
|
+
expect(outcome.kind === "answered" ? await outcome.response.text() : undefined).toBe("status 503");
|
|
55
|
+
expect(tries).toEqual([1, 2, 3]);
|
|
56
|
+
// The last try is not retried, so it is not narrated as one.
|
|
57
|
+
expect(heard).toEqual([1, 2]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("is unreachable, with the last try's failure, when nothing ever answered", async () => {
|
|
61
|
+
const { tries, send } = scripted(["offline"]);
|
|
62
|
+
const outcome = await redeemWhileWarming(send, { attempts: 2, delayMs: 0 });
|
|
63
|
+
expect(outcome.kind).toBe("unreachable");
|
|
64
|
+
expect(outcome.kind === "unreachable" ? outcome.error : undefined).toBeInstanceOf(TypeError);
|
|
65
|
+
expect(tries).toEqual([1, 2]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("abandons a try that outlives its timeout and asks again", async () => {
|
|
69
|
+
const { tries, send } = scripted(["hang", 200]);
|
|
70
|
+
expect(statusOf(await redeemWhileWarming(send, { attempts: 2, delayMs: 0, timeoutMs: 10 }))).toBe(200);
|
|
71
|
+
expect(tries).toEqual([1, 2]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects with the stop's reason rather than answering when its signal aborts", async () => {
|
|
75
|
+
const stop = new AbortController();
|
|
76
|
+
const { tries, send } = scripted([502]);
|
|
77
|
+
const running = redeemWhileWarming(send, {
|
|
78
|
+
attempts: 5,
|
|
79
|
+
delayMs: 60_000,
|
|
80
|
+
signal: stop.signal,
|
|
81
|
+
onRetry: () => stop.abort(new Error("stopped")),
|
|
82
|
+
});
|
|
83
|
+
await expect(running).rejects.toThrow("stopped");
|
|
84
|
+
expect(tries).toEqual([1]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { retry, withDeadline } from "@intentic/base/async";
|
|
2
|
+
|
|
3
|
+
// Redeeming a pairing code's one-time token, as every peer that takes one does it (the machine agent's
|
|
4
|
+
// `postWhileWarming`, the webext's `redeemPairing`): a sandbox still warming up answers 5xx through its tunnel, or
|
|
5
|
+
// nothing at all, and is asked again. Any other answer, a 401 above all (the code has expired), is the caller's to read
|
|
6
|
+
// at once: asking again would only delay the fresh code the person has to fetch.
|
|
7
|
+
|
|
8
|
+
// Why a try is being asked again: no answer at all, or a 5xx from a sandbox not ready yet.
|
|
9
|
+
export type RedeemRetry = { readonly kind: "unreachable"; readonly error: unknown } | { readonly kind: "warming"; readonly status: number };
|
|
10
|
+
|
|
11
|
+
export type RedeemOutcome =
|
|
12
|
+
// The sandbox's answer: below 500 at once, or still 5xx when the tries ran out.
|
|
13
|
+
| { readonly kind: "answered"; readonly response: Response }
|
|
14
|
+
// No answer at all on the last try; `error` is that try's failure.
|
|
15
|
+
| { readonly kind: "unreachable"; readonly error: unknown };
|
|
16
|
+
|
|
17
|
+
export interface RedeemOptions {
|
|
18
|
+
// Tries in all, the first included.
|
|
19
|
+
readonly attempts: number;
|
|
20
|
+
// The wait between tries.
|
|
21
|
+
readonly delayMs: number;
|
|
22
|
+
// How long one try may take before it is abandoned and counts as unreachable; none when absent.
|
|
23
|
+
readonly timeoutMs?: number | undefined;
|
|
24
|
+
// Stops the tries: a stop rejects with the signal's reason rather than answering.
|
|
25
|
+
readonly signal?: AbortSignal | undefined;
|
|
26
|
+
// Hears each try that is about to be asked again, before the wait.
|
|
27
|
+
readonly onRetry?: ((attempt: number, why: RedeemRetry) => void) | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A 5xx carried out of a try so `retry` asks again, and handed back as the answer when the tries run out.
|
|
31
|
+
class Warming extends Error {
|
|
32
|
+
constructor(readonly response: Response) {
|
|
33
|
+
super(`the sandbox is warming up (HTTP ${response.status})`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Runs `send` (one POST, handed the try's number and a signal to pass to its fetch, undefined when there is neither a
|
|
38
|
+
// deadline nor a stop) until the sandbox answers below 500 or the tries run out.
|
|
39
|
+
export const redeemWhileWarming = async (
|
|
40
|
+
send: (attempt: number, signal: AbortSignal | undefined) => Promise<Response>,
|
|
41
|
+
options: RedeemOptions,
|
|
42
|
+
): Promise<RedeemOutcome> => {
|
|
43
|
+
const { attempts, delayMs, timeoutMs, signal, onRetry } = options;
|
|
44
|
+
const once = (attempt: number): Promise<Response> =>
|
|
45
|
+
timeoutMs === undefined ? send(attempt, signal) : withDeadline((bounded) => send(attempt, bounded), timeoutMs, { signal });
|
|
46
|
+
try {
|
|
47
|
+
const response = await retry(
|
|
48
|
+
async (attempt) => {
|
|
49
|
+
let answer: Response;
|
|
50
|
+
try {
|
|
51
|
+
answer = await once(attempt);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (attempt < attempts && signal?.aborted !== true) {
|
|
54
|
+
onRetry?.(attempt, { kind: "unreachable", error });
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
if (answer.status < 500) {
|
|
59
|
+
return answer;
|
|
60
|
+
}
|
|
61
|
+
if (attempt < attempts) {
|
|
62
|
+
onRetry?.(attempt, { kind: "warming", status: answer.status });
|
|
63
|
+
}
|
|
64
|
+
throw new Warming(answer);
|
|
65
|
+
},
|
|
66
|
+
{ attempts, delayMs: () => delayMs, signal },
|
|
67
|
+
);
|
|
68
|
+
return { kind: "answered", response };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (signal?.aborted === true) {
|
|
71
|
+
// oxlint-disable-next-line typescript/only-throw-error -- the stop's own reason, as an aborted fetch rejects with it
|
|
72
|
+
throw signal.reason;
|
|
73
|
+
}
|
|
74
|
+
if (error instanceof Warming) {
|
|
75
|
+
return { kind: "answered", response: error.response };
|
|
76
|
+
}
|
|
77
|
+
return { kind: "unreachable", error };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// namespace keeps zod's 60 locales (~250 kB) that esbuild can otherwise drop. _devices/webext/scripts/size-budget.mjs holds the ceiling.
|
|
3
3
|
import { prettifyError, toJSONSchema } from "zod";
|
|
4
4
|
import type * as z from "zod";
|
|
5
|
+
import { isRecord } from "@intentic/base/escape";
|
|
5
6
|
|
|
6
7
|
// The MCP version every peer announces, here rather than in host-protocol.ts because a browser extension bundles this
|
|
7
8
|
// module and would otherwise carry the machine handshake's schemas to a store. Also read by the daemon's peer bridge,
|
|
@@ -99,8 +100,6 @@ export interface McpServerSpec<Ctx> {
|
|
|
99
100
|
readonly audit: (entry: McpAuditEntry) => Promise<void> | void;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
103
|
-
|
|
104
103
|
// Handles one JSON-RPC message; returns undefined for a notification. `ctx` is read per call, so a grant pushed
|
|
105
104
|
// mid-session takes effect on the next call.
|
|
106
105
|
export const createMcpServer = <Ctx>(spec: McpServerSpec<Ctx>): ((message: unknown, ctx: Ctx) => Promise<Record<string, unknown> | undefined>) => {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../../schemas/environment.js";
|
|
11
11
|
import { MemberEmailSchema, MemberGrantSchema, MembersListSchema, MembersRosterSchema } from "../../schemas/members.js";
|
|
12
12
|
import { OkSchema } from "../../schemas/shared.js";
|
|
13
|
+
import { EnrollCodeAnswerSchema, EnrollCodeRequestSchema } from "../../schemas/short-codes.js";
|
|
13
14
|
import { DefinitionDiffSchema, DefinitionExportSchema, WorkspacePublishResultSchema, WorkspacePublishSchema, WorkspaceRemoteSchema } from "../../state/definition.js";
|
|
14
15
|
import type { RawRouteKey } from "./raw-routes.js";
|
|
15
16
|
|
|
@@ -24,6 +25,8 @@ export interface RawJsonRoute {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export const RAW_JSON_ROUTES = {
|
|
28
|
+
// A server-connect code for the owner's command (short-codes.ts).
|
|
29
|
+
"POST /system/enroll/code": { input: EnrollCodeRequestSchema, output: EnrollCodeAnswerSchema },
|
|
27
30
|
// The roster is ownership's; every write answers the roster after it.
|
|
28
31
|
"GET /members": { output: MembersRosterSchema },
|
|
29
32
|
"POST /members": { input: MemberGrantSchema, output: MembersListSchema },
|