@intentic/sandbox-contract 1.317.0 → 1.319.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 +16 -6
- package/dist/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/probes.js +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/contracts/accounts.contract.d.ts +75 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/accounts.contract.js +10 -1
- package/dist/contracts/accounts.contract.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +62 -1
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +830 -93
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +12 -12
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/ci.contract.d.ts +22 -4
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +38 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.js +2 -0
- package/dist/contracts/device.contract.js.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +23 -0
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.js +6 -1
- package/dist/contracts/endpoints.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +12 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +2 -0
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.d.ts +4 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +143 -124
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +37 -0
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.js +20 -1
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +4 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +31 -4
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +20 -1
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +102 -464
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +2 -12
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/translator.contract.d.ts +7 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.js.map +1 -1
- package/dist/contracts/webext.contract.d.ts +38 -0
- package/dist/contracts/webext.contract.d.ts.map +1 -1
- package/dist/contracts/webext.contract.js +3 -0
- package/dist/contracts/webext.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +1 -133
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +1 -37
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +21 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +11 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts +1 -0
- 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/land-breakage.d.ts.map +1 -1
- package/dist/events/land-breakage.js.map +1 -1
- package/dist/events/resume.d.ts +6 -1
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +17 -10
- package/dist/events/resume.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +183 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -0
- package/dist/events/sandbox-notice.js +56 -0
- package/dist/events/sandbox-notice.js.map +1 -0
- package/dist/events/system-events.d.ts +50 -18
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +4 -2
- package/dist/events/system-events.js.map +1 -1
- package/dist/events/transcript.d.ts +24 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +3 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/front/generated/browser-wire.d.ts +13 -0
- package/dist/front/generated/browser-wire.d.ts.map +1 -1
- package/dist/front/generated/wire.d.ts +4 -0
- package/dist/front/generated/wire.d.ts.map +1 -1
- package/dist/ids/conversation-ids.d.ts +1 -3
- package/dist/ids/conversation-ids.d.ts.map +1 -1
- package/dist/ids/conversation-ids.js +1 -3
- package/dist/ids/conversation-ids.js.map +1 -1
- package/dist/ids/project-dir.d.ts +6 -0
- package/dist/ids/project-dir.d.ts.map +1 -0
- package/dist/ids/project-dir.js +43 -0
- package/dist/ids/project-dir.js.map +1 -0
- package/dist/index.d.ts +11543 -11039
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +2 -2
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/model-roles.d.ts +1 -9
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +1 -9
- package/dist/models/model-roles.js.map +1 -1
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +52 -3
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/safety-policy.d.ts +7 -1
- package/dist/policy/safety-policy.d.ts.map +1 -1
- package/dist/policy/safety-policy.js +2 -0
- package/dist/policy/safety-policy.js.map +1 -1
- package/dist/policy/secret-hosts.d.ts +13 -0
- package/dist/policy/secret-hosts.d.ts.map +1 -0
- package/dist/policy/secret-hosts.js +41 -0
- package/dist/policy/secret-hosts.js.map +1 -0
- 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/raw-routes.d.ts +12 -0
- package/dist/protocol/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw-routes.js +5 -0
- package/dist/protocol/raw-routes.js.map +1 -1
- package/dist/protocol/routes.d.ts +2 -6
- package/dist/protocol/routes.d.ts.map +1 -1
- package/dist/protocol/routes.js +2 -3
- package/dist/protocol/routes.js.map +1 -1
- package/dist/protocol/stream-of.d.ts +6 -0
- package/dist/protocol/stream-of.d.ts.map +1 -0
- package/dist/protocol/stream-of.js +4 -0
- package/dist/protocol/stream-of.js.map +1 -0
- package/dist/protocol/vitals.d.ts +8 -0
- package/dist/protocol/vitals.d.ts.map +1 -0
- package/dist/protocol/vitals.js +16 -0
- package/dist/protocol/vitals.js.map +1 -0
- package/dist/schemas/activity.js +1 -1
- package/dist/schemas/activity.js.map +1 -1
- package/dist/schemas/agent.d.ts +25 -2
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +14 -3
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +67 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +36 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +20 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/capabilities.d.ts +2 -0
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +34 -4
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/ci.d.ts +69 -9
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +29 -8
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/context-trim.d.ts +5 -1
- package/dist/schemas/context-trim.d.ts.map +1 -1
- package/dist/schemas/context-trim.js +8 -4
- package/dist/schemas/context-trim.js.map +1 -1
- package/dist/schemas/devices.d.ts +23 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +16 -1
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/environment.d.ts +48 -0
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +15 -0
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/extension-updates.d.ts +24 -0
- package/dist/schemas/extension-updates.d.ts.map +1 -1
- package/dist/schemas/git/git.d.ts +13 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +11 -1
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +2 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +2 -1
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/keep-warm.d.ts +4 -0
- package/dist/schemas/keep-warm.d.ts.map +1 -1
- package/dist/schemas/keep-warm.js +15 -5
- package/dist/schemas/keep-warm.js.map +1 -1
- package/dist/schemas/loopback-catch.d.ts +33 -0
- package/dist/schemas/loopback-catch.d.ts.map +1 -0
- package/dist/schemas/loopback-catch.js +20 -0
- package/dist/schemas/loopback-catch.js.map +1 -0
- package/dist/schemas/maintenance.js +1 -1
- package/dist/schemas/maintenance.js.map +1 -1
- package/dist/schemas/needs.d.ts +3 -0
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/needs.js +10 -3
- package/dist/schemas/needs.js.map +1 -1
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +6 -3
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +87 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +20 -0
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/secrets.d.ts +66 -0
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +50 -0
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/settings-history.d.ts +2 -2
- package/dist/schemas/settings-history.d.ts.map +1 -1
- package/dist/schemas/settings-history.js +2 -0
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +42 -4
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +26 -8
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/terminal.d.ts +2 -3
- package/dist/schemas/terminal.d.ts.map +1 -1
- package/dist/schemas/terminal.js +12 -4
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/webext.d.ts +3 -0
- package/dist/schemas/webext.d.ts.map +1 -1
- package/dist/schemas/webext.js +1 -0
- package/dist/schemas/webext.js.map +1 -1
- package/dist/schemas/workflows.js +3 -3
- package/dist/schemas/workflows.js.map +1 -1
- package/dist/schemas/workspace/workspace-tree.d.ts +2 -56
- package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-tree.js +7 -15
- package/dist/schemas/workspace/workspace-tree.js.map +1 -1
- package/dist/state/definition.d.ts +12 -8
- 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 +7 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +0 -3
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +0 -1
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +0 -5
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +0 -6
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/documents.d.ts +3 -0
- package/dist/text/documents.d.ts.map +1 -1
- package/dist/text/documents.js +14 -0
- package/dist/text/documents.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +4 -3
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +137 -31
- package/dist/text/transcript-fold.js.map +1 -1
- package/dist/webext/index.d.ts +1 -0
- package/dist/webext/index.d.ts.map +1 -1
- package/dist/webext/index.js +1 -0
- package/dist/webext/index.js.map +1 -1
- package/package.json +5 -5
- package/src/chores/chores.ts +2 -2
- package/src/chores/probes.ts +1 -1
- package/src/contracts/accounts.contract.ts +12 -0
- package/src/contracts/agents.contract.ts +13 -13
- package/src/contracts/device.contract.ts +5 -0
- package/src/contracts/endpoints.contract.ts +20 -4
- package/src/contracts/secrets.contract.ts +28 -0
- package/src/contracts/settings.contract.ts +29 -0
- package/src/contracts/system.contract.ts +5 -14
- package/src/contracts/translator.contract.ts +2 -1
- package/src/contracts/webext.contract.ts +7 -1
- package/src/contracts/workspace.contract.ts +0 -49
- package/src/events/agent-events.ts +18 -1
- package/src/events/agent-words.ts +4 -1
- package/src/events/land-breakage.ts +3 -4
- package/src/events/resume.test.ts +13 -2
- package/src/events/resume.ts +25 -12
- package/src/events/sandbox-notice.ts +92 -0
- package/src/events/system-events.ts +12 -5
- package/src/events/transcript.ts +7 -1
- package/src/events/verify-nudge.ts +3 -3
- package/src/front/generated/browser-wire.json +95 -0
- package/src/front/generated/browser-wire.ts +36 -0
- package/src/front/generated/front-wire.json +25 -0
- package/src/front/generated/wire.ts +2 -2
- package/src/front/wire-manifests.test.ts +7 -0
- package/src/ids/conversation-ids.test.ts +2 -26
- package/src/ids/conversation-ids.ts +6 -12
- package/src/ids/project-dir.fixture.json +65 -0
- package/src/ids/project-dir.test.ts +60 -0
- package/src/ids/project-dir.ts +70 -0
- package/src/index.ts +13 -4
- package/src/models/agent-catalog.test.ts +7 -1
- package/src/models/agent-runtimes.ts +7 -5
- package/src/models/model-roles.ts +1 -9
- package/src/policy/command-classes.test.ts +39 -0
- package/src/policy/command-classes.ts +70 -3
- package/src/policy/safety-policy.ts +10 -0
- package/src/policy/secret-hosts.test.ts +89 -0
- package/src/policy/secret-hosts.ts +73 -0
- package/src/policy/turned-away.ts +1 -0
- package/src/protocol/raw-routes.ts +8 -1
- package/src/protocol/routes.test.ts +6 -6
- package/src/protocol/routes.ts +2 -9
- package/src/protocol/stream-of.ts +13 -0
- package/src/protocol/vitals.test.ts +46 -0
- package/src/protocol/vitals.ts +38 -0
- package/src/schemas/activity.ts +1 -1
- package/src/schemas/agent.ts +20 -3
- package/src/schemas/agents.ts +58 -4
- package/src/schemas/automations.ts +2 -2
- package/src/schemas/capabilities.ts +44 -7
- package/src/schemas/ci.ts +56 -11
- package/src/schemas/context-trim.ts +11 -6
- package/src/schemas/devices.ts +38 -2
- package/src/schemas/environment.ts +39 -0
- package/src/schemas/git/git.ts +20 -2
- package/src/schemas/hosts.ts +4 -1
- package/src/schemas/keep-warm.ts +19 -6
- package/src/schemas/loopback-catch.ts +45 -0
- package/src/schemas/maintenance.ts +1 -1
- package/src/schemas/needs.ts +12 -3
- package/src/schemas/personas.ts +8 -3
- package/src/schemas/providers/provider-oauth.ts +35 -0
- package/src/schemas/secrets.ts +70 -0
- package/src/schemas/settings-history.ts +3 -0
- package/src/schemas/settings.test.ts +19 -1
- package/src/schemas/settings.ts +59 -16
- package/src/schemas/speaker.ts +5 -5
- package/src/schemas/terminal.ts +19 -6
- package/src/schemas/webext.ts +3 -0
- package/src/schemas/workflows.ts +5 -5
- package/src/schemas/workspace/workspace-tree.ts +12 -20
- package/src/state/fix-attempt-plan.test.ts +1 -1
- package/src/state/fix-stance.ts +1 -1
- package/src/state/history-state.ts +14 -0
- package/src/state/runtime-state.ts +0 -3
- package/src/state/workspace-state.ts +0 -6
- package/src/text/documents.test.ts +25 -1
- package/src/text/documents.ts +24 -0
- package/src/text/title.ts +1 -1
- package/src/text/transcript-fold.test.ts +218 -25
- package/src/text/transcript-fold.ts +166 -45
- package/src/webext/index.ts +1 -0
- package/dist/schemas/workspace/push-checks.d.ts +0 -222
- package/dist/schemas/workspace/push-checks.d.ts.map +0 -1
- package/dist/schemas/workspace/push-checks.js +0 -97
- package/dist/schemas/workspace/push-checks.js.map +0 -1
- package/dist/state/red-streak.d.ts +0 -7
- package/dist/state/red-streak.d.ts.map +0 -1
- package/dist/state/red-streak.js +0 -11
- package/dist/state/red-streak.js.map +0 -1
- package/src/schemas/workspace/push-checks.test.ts +0 -28
- package/src/schemas/workspace/push-checks.ts +0 -155
- package/src/state/red-streak.test.ts +0 -28
- package/src/state/red-streak.ts +0 -28
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { MEMORY_FILE, PUBLIC_DIR, REFERENCE_DIR, WORKSPACE_ROOT } from "@intentic/constants";
|
|
2
|
+
import { STARTER_REPO } from "../state/starter.js";
|
|
3
|
+
|
|
4
|
+
// A PROJECT SANDBOX is made for one folder on the owner's computer, synced live into `/work/<name>` rather than into
|
|
5
|
+
// /work itself: the workspace root is the daemon's own, where it keeps its state, seeds its starter repo and serves
|
|
6
|
+
// `public/` to anyone with a link. `<name>` is derived from the folder's name once, on the owner's side, and every
|
|
7
|
+
// later reader (the sync agent, `ic`, the daemon) only validates it. `_sandbox/ic/src/sandbox/project_dir.rs` holds the
|
|
8
|
+
// validation again for the host side; `project-dir.fixture.json` is the one set of cases both run.
|
|
9
|
+
|
|
10
|
+
// Top-level names the daemon treats as its own, so a project can never land on one: the public outbox, the reference
|
|
11
|
+
// shelf, the starter repo seeded on first boot, the root repo's id and the three repo roles repo discovery refuses
|
|
12
|
+
// (`_sandbox/sandbox/src/workspace/layout/repo-discovery.ts`), and the standing-instructions file it writes. Then the
|
|
13
|
+
// names every walk of the workspace skips at any depth (@intentic/workspace-ignore IGNORED_DIRS, and the private file
|
|
14
|
+
// the git scopes leave out), where a project would sync and never be seen; the daemon's tests hold this list to that
|
|
15
|
+
// one. Everything else of either kind starts with a dot (`.intentic`, `.venv`), which the name rule below refuses.
|
|
16
|
+
export const RESERVED_PROJECT_DIR_NAMES: readonly string[] = [
|
|
17
|
+
PUBLIC_DIR,
|
|
18
|
+
REFERENCE_DIR,
|
|
19
|
+
STARTER_REPO,
|
|
20
|
+
"root",
|
|
21
|
+
"intent",
|
|
22
|
+
"desired-state",
|
|
23
|
+
"app",
|
|
24
|
+
MEMORY_FILE,
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"venv",
|
|
28
|
+
"claude.json",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Starts alphanumeric, so no dot-file and no leading dash an argv could read as a flag; no separator, so it is always
|
|
32
|
+
// one path segment; bounded, since it also names a git dir and a folder in every view of the workspace.
|
|
33
|
+
const PROJECT_DIR_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
34
|
+
const PROJECT_DIR_NAME_MAX = 64;
|
|
35
|
+
|
|
36
|
+
// Used when nothing of the folder's name survives the slug (a name in another script, or only punctuation).
|
|
37
|
+
const FALLBACK_NAME = "project";
|
|
38
|
+
|
|
39
|
+
// Combining marks, which NFKD splits off an accented letter.
|
|
40
|
+
const COMBINING_MARKS = /[\u0300-\u036f]/g;
|
|
41
|
+
|
|
42
|
+
export const isProjectDirName = (name: string): boolean => PROJECT_DIR_NAME.test(name) && !RESERVED_PROJECT_DIR_NAMES.includes(name);
|
|
43
|
+
|
|
44
|
+
// The workspace-relative name for a folder the owner picked, always one `isProjectDirName` accepts. Accents are folded
|
|
45
|
+
// to their base letter first, so `Café` keeps its `e` rather than losing it to a dash.
|
|
46
|
+
export const projectDirNameFor = (folderBasename: string): string => {
|
|
47
|
+
const slug = folderBasename
|
|
48
|
+
.normalize("NFKD")
|
|
49
|
+
.replace(COMBINING_MARKS, "")
|
|
50
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
51
|
+
.replace(/-{2,}/g, "-")
|
|
52
|
+
.replace(/^[._-]+/, "")
|
|
53
|
+
.slice(0, PROJECT_DIR_NAME_MAX)
|
|
54
|
+
.replace(/[.-]+$/, "");
|
|
55
|
+
const name = slug === "" ? FALLBACK_NAME : slug;
|
|
56
|
+
return RESERVED_PROJECT_DIR_NAMES.includes(name) ? `${name}-project` : name;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Where a project folder lands in the sandbox, and the name back out of such a path: exactly `<root>/<name>`, anything
|
|
60
|
+
// else (a deeper path, a trailing slash, a reserved name) is not a project dir.
|
|
61
|
+
export const projectRemoteDir = (name: string): string => `${WORKSPACE_ROOT}/${name}`;
|
|
62
|
+
|
|
63
|
+
export const projectDirNameOf = (remoteDir: string, root: string = WORKSPACE_ROOT): string | undefined => {
|
|
64
|
+
const prefix = `${root}/`;
|
|
65
|
+
if (!remoteDir.startsWith(prefix)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const name = remoteDir.slice(prefix.length);
|
|
69
|
+
return isProjectDirName(name) ? name : undefined;
|
|
70
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,8 @@ export { choresContract } from "./contracts/chores.contract.js";
|
|
|
58
58
|
export { ciContract } from "./contracts/ci.contract.js";
|
|
59
59
|
export {
|
|
60
60
|
endpointsContract,
|
|
61
|
+
type LocalModelDevice,
|
|
62
|
+
LocalModelDeviceSchema,
|
|
61
63
|
LocalModelFitSchema,
|
|
62
64
|
type LocalModelFitResponse,
|
|
63
65
|
type LocalModelGpu,
|
|
@@ -86,6 +88,7 @@ export { issuesContract } from "./contracts/issues.contract.js";
|
|
|
86
88
|
export { logsContract } from "./contracts/logs.contract.js";
|
|
87
89
|
export { REQUEST_ID_HEADER } from "./protocol/request-id.js";
|
|
88
90
|
export * from "./protocol/edge-verdict.js";
|
|
91
|
+
export * from "./protocol/vitals.js";
|
|
89
92
|
export { loopsContract } from "./contracts/loops.contract.js";
|
|
90
93
|
export { panelsContract } from "./contracts/panels.contract.js";
|
|
91
94
|
export { portsContract } from "./contracts/ports.contract.js";
|
|
@@ -111,6 +114,7 @@ export * from "./events/agent-events.js";
|
|
|
111
114
|
export * from "./events/agent-words.js";
|
|
112
115
|
export * from "./events/requests.js";
|
|
113
116
|
export * from "./events/resume.js";
|
|
117
|
+
export * from "./events/sandbox-notice.js";
|
|
114
118
|
export * from "./events/system-events.js";
|
|
115
119
|
export * from "./events/transcript.js";
|
|
116
120
|
export * from "./events/land-breakage.js";
|
|
@@ -139,7 +143,6 @@ export * from "./state/history-state.js";
|
|
|
139
143
|
export * from "./state/fix-stance.js";
|
|
140
144
|
export * from "./state/landed-commit.js";
|
|
141
145
|
export * from "./state/fix-attempt-plan.js";
|
|
142
|
-
export * from "./state/red-streak.js";
|
|
143
146
|
// Provider vocabulary bottom-up: agent-runtimes, provider-specs, agent-catalog; three modules since the dependency
|
|
144
147
|
// points one way.
|
|
145
148
|
export * from "./models/agent-runtimes.js";
|
|
@@ -152,6 +155,7 @@ export * from "./policy/safety-policy.js";
|
|
|
152
155
|
export * from "./policy/reserved-servers.js";
|
|
153
156
|
export * from "./policy/credential-material.js";
|
|
154
157
|
export * from "./policy/capability-secrets.js";
|
|
158
|
+
export * from "./policy/secret-hosts.js";
|
|
155
159
|
export * from "./ids/conversation-ids.js";
|
|
156
160
|
export * from "./text/documents.js";
|
|
157
161
|
export * from "./protocol/host-protocol.js";
|
|
@@ -161,6 +165,7 @@ export * from "./protocol/runner-protocol.js";
|
|
|
161
165
|
export * from "./protocol/listener-protocol.js";
|
|
162
166
|
export * from "./protocol/container-requirements.js";
|
|
163
167
|
export * from "./ids/hostnames.js";
|
|
168
|
+
export * from "./ids/project-dir.js";
|
|
164
169
|
export * from "./policy/overlay-lint.js";
|
|
165
170
|
export * from "./models/model-label.js";
|
|
166
171
|
export * from "./models/model-order.js";
|
|
@@ -200,6 +205,7 @@ export * from "./schemas/git/git.js";
|
|
|
200
205
|
export * from "./schemas/git/git-history.js";
|
|
201
206
|
export * from "./schemas/history.js";
|
|
202
207
|
export * from "./schemas/hosts.js";
|
|
208
|
+
export * from "./schemas/loopback-catch.js";
|
|
203
209
|
export * from "./schemas/intentic.js";
|
|
204
210
|
export * from "./schemas/inventory.js";
|
|
205
211
|
export * from "./schemas/issues.js";
|
|
@@ -239,7 +245,6 @@ export * from "./schemas/webext.js";
|
|
|
239
245
|
export * from "./schemas/workflows.js";
|
|
240
246
|
export * from "./schemas/workspace/workspace-repos.js";
|
|
241
247
|
export * from "./schemas/workspace/workspace-search.js";
|
|
242
|
-
export * from "./schemas/workspace/push-checks.js";
|
|
243
248
|
export * from "./schemas/workspace/workspace-setup.js";
|
|
244
249
|
export * from "./schemas/workspace/workspace-tree.js";
|
|
245
250
|
export * from "./state/arrival.js";
|
|
@@ -308,8 +313,12 @@ export const SANDBOX_ROUTES: readonly ContractRoute[] = contractRoutes(sandboxCo
|
|
|
308
313
|
export const SANDBOX_ROUTE_NAMES: readonly string[] = SANDBOX_ROUTES.map((route) => route.name);
|
|
309
314
|
|
|
310
315
|
// Each route's shape, for the failure names alone can't describe: a route both builds have, but whose payload only one
|
|
311
|
-
// expects. Computed
|
|
312
|
-
|
|
316
|
+
// expects. Computed on the first ask and kept, since it's too expensive to repeat per connection — and too expensive to
|
|
317
|
+
// pay at load: turning every route's schemas into JSON Schema was a third of a second of a phone's startup at 4× CPU
|
|
318
|
+
// throttle, for a comparison a browser only makes when it has a disagreement to explain (the version card). The daemon
|
|
319
|
+
// asks on its first hello.
|
|
320
|
+
let fingerprints: Readonly<Record<string, string>> | undefined;
|
|
321
|
+
export const sandboxRouteFingerprints = (): Readonly<Record<string, string>> => (fingerprints ??= routeShapes(sandboxContract));
|
|
313
322
|
|
|
314
323
|
// The contract route a concrete browser request belongs to, bound to this build's route table.
|
|
315
324
|
export const sandboxRouteName = (method: string, pathWithQuery: string): string | undefined =>
|
|
@@ -126,6 +126,9 @@ test("the ceiling has nothing to disclose; a floor names what it lacks", () => {
|
|
|
126
126
|
expect(grokCaps.steering).toBe(false);
|
|
127
127
|
expect(grokCaps.isolation).toBe("cwd");
|
|
128
128
|
expect(limitationsOf(grokCaps).length).toBeGreaterThan(0);
|
|
129
|
+
// The turn's remote servers, as Codex takes them: mounted on the shared OpenCode server per conversation.
|
|
130
|
+
expect(grokCaps.mcp).toBe("http");
|
|
131
|
+
expect(limitationsOf(grokCaps)).toContain("MCP servers only, none of the daemon's own tools, no plugins");
|
|
129
132
|
|
|
130
133
|
const acpCaps = capabilitiesOf("some-installed-agent", "native");
|
|
131
134
|
expect(acpCaps.mcp).toBe("http");
|
|
@@ -178,7 +181,10 @@ test("the safety axes disclose the middle answer differently from the floor", ()
|
|
|
178
181
|
|
|
179
182
|
expect(codexCaps.rulebook).toBe("approval");
|
|
180
183
|
expect(piCaps.rulebook).toBe("none");
|
|
181
|
-
expect(grokCaps.rulebook).toBe("
|
|
184
|
+
expect(grokCaps.rulebook).toBe("approval");
|
|
185
|
+
// A hold parks on a card now that the turn's watchdog is held while it waits, so nothing says it cannot hold.
|
|
186
|
+
expect(limitationsOf(grokCaps)).toContain("command rules apply only to calls this agent raises");
|
|
187
|
+
expect(limitationsOf(grokCaps)).not.toContain("command rules can refuse but not hold");
|
|
182
188
|
|
|
183
189
|
expect(limitationsOf(claudeCaps).join(" ")).not.toContain("command rules");
|
|
184
190
|
expect(limitationsOf(claudeCaps).join(" ")).not.toContain("stored secrets");
|
|
@@ -102,14 +102,15 @@ export const CODEX: AgentCapabilities = {
|
|
|
102
102
|
secrets: "none",
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
// OpenCode (the Grok runtime): its own agentic loop
|
|
106
|
-
// and one system message of ours; no effort scale, no
|
|
105
|
+
// OpenCode (the Grok runtime): its own agentic loop and tools, with the turn's remote MCP servers mounted on the shared
|
|
106
|
+
// server per conversation. Takes a model id, a prompt and one system message of ours; no effort scale, no command list.
|
|
107
107
|
export const OPENCODE: AgentCapabilities = {
|
|
108
108
|
runtime: "opencode",
|
|
109
109
|
steering: false,
|
|
110
110
|
permissions: "plan",
|
|
111
111
|
questions: false,
|
|
112
|
-
|
|
112
|
+
// Added at runtime under the conversation's own names, each prompt showing its session only its own (opencode-mcp.ts).
|
|
113
|
+
mcp: "http",
|
|
113
114
|
execution: ["shell"],
|
|
114
115
|
effort: false,
|
|
115
116
|
fastMode: false,
|
|
@@ -121,8 +122,9 @@ export const OPENCODE: AgentCapabilities = {
|
|
|
121
122
|
// `system` on the prompt body, per message; adds to OpenCode's own prompt, with no seam to replace it.
|
|
122
123
|
instructions: "append",
|
|
123
124
|
skillDiscovery: "prompt",
|
|
124
|
-
//
|
|
125
|
-
|
|
125
|
+
// OpenCode raises its permission asks for the commands its config names; the daemon answers from the rulebook, a
|
|
126
|
+
// hold parked on a card while the turn's inactivity watchdog is held.
|
|
127
|
+
rulebook: "approval",
|
|
126
128
|
secrets: "none",
|
|
127
129
|
};
|
|
128
130
|
|
|
@@ -71,7 +71,7 @@ export const MODEL_ROLES = [
|
|
|
71
71
|
{
|
|
72
72
|
id: "pipeline-fix",
|
|
73
73
|
label: "Pipeline fixes",
|
|
74
|
-
blurb: "The agent started by Fix on a
|
|
74
|
+
blurb: "The agent started by Fix on a failed pipeline.",
|
|
75
75
|
kind: "run",
|
|
76
76
|
trigger: "pressed",
|
|
77
77
|
icon: "wave-pulse",
|
|
@@ -108,14 +108,6 @@ export const MODEL_ROLES = [
|
|
|
108
108
|
trigger: "pressed",
|
|
109
109
|
icon: "list-check",
|
|
110
110
|
},
|
|
111
|
-
{
|
|
112
|
-
id: "pre-push-fix",
|
|
113
|
-
label: "Pre-push fixes",
|
|
114
|
-
blurb: "The fix proposed when a check fails on the way to a push.",
|
|
115
|
-
kind: "run",
|
|
116
|
-
trigger: "pressed",
|
|
117
|
-
icon: "cloud-upload",
|
|
118
|
-
},
|
|
119
111
|
{
|
|
120
112
|
// Pressed trigger: the owner's approval click starts this turn; the queue between is just machinery.
|
|
121
113
|
id: "approval-queue",
|
|
@@ -40,6 +40,22 @@ describe("git.destructive", () => {
|
|
|
40
40
|
test("a -f belonging to the next command in a pipeline is not a force-push", () => {
|
|
41
41
|
expect(classify("git push origin main | grep -f patterns.txt")).not.toContain("git.destructive");
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
test("deleting a checkout's .git, or re-initialising the folder it stands in, cuts it off from its history", () => {
|
|
45
|
+
for (const command of ["rm .git", "rm -f .git", "rm -rf ./.git", "rm -rf tabularium/.git", "rm .git && git init -b main"]) {
|
|
46
|
+
expect(classify(command), command).toContain("git.destructive");
|
|
47
|
+
}
|
|
48
|
+
for (const command of ["git init", "git init .", "git init -b main", "git init --initial-branch main -q ."]) {
|
|
49
|
+
expect(classify(command), command).toContain("git.destructive");
|
|
50
|
+
}
|
|
51
|
+
expect(marked("cd /work && rm .git && git init -b main", "git.destructive")).toEqual(["rm .git", "git init -b main"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("a new project's git init, and files that merely start with .git, are left alone", () => {
|
|
55
|
+
for (const command of ["git init new-project", "git init -b main tools/cli", "rm .gitignore", "rm -f .github/old.yml", "rm .gitmodules"]) {
|
|
56
|
+
expect(classify(command), command).not.toContain("git.destructive");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
43
59
|
});
|
|
44
60
|
|
|
45
61
|
describe("files.destructive", () => {
|
|
@@ -72,6 +88,27 @@ describe("files.destructive", () => {
|
|
|
72
88
|
}
|
|
73
89
|
});
|
|
74
90
|
|
|
91
|
+
test("find deleting what it walks, or handing it to rm, is the same delete as rm -rf", () => {
|
|
92
|
+
for (const command of [
|
|
93
|
+
"find /home/x/PROJECT/cerberus-worktrees/m1-a -depth -delete",
|
|
94
|
+
"find build -name '*.o' -delete",
|
|
95
|
+
"find . -type d -name node_modules -exec rm -rf {} +",
|
|
96
|
+
"find dist -exec /bin/rm -f {} \\;",
|
|
97
|
+
"find tmp -execdir rm {} \\;",
|
|
98
|
+
"find . -name '*.log' -print0 | xargs -0 rm -f",
|
|
99
|
+
"ls old | xargs rm",
|
|
100
|
+
]) {
|
|
101
|
+
expect(classify(command), command).toContain("files.destructive");
|
|
102
|
+
}
|
|
103
|
+
expect(marked("find build -name '*.o' -delete; echo done", "files.destructive")).toEqual(["find build -name '*.o' -delete"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a find that only lists, and an xargs that runs something else, pass", () => {
|
|
107
|
+
for (const command of ["find . -name '*.ts'", "find . -newer deleted.txt -print", "find src -exec grep -l rm {} +", "ls | xargs wc -l"]) {
|
|
108
|
+
expect(classify(command), command).not.toContain("files.destructive");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
75
112
|
test("a script that deletes one file, or nothing, is not recursive deletion", () => {
|
|
76
113
|
for (const code of ['fs.unlinkSync("tmp.txt")', 'await fs.promises.rm("tmp.txt")', "const rmq = queue.rm(job)"]) {
|
|
77
114
|
expect(classify(code), code).not.toContain("files.destructive");
|
|
@@ -113,6 +150,8 @@ describe("system.destructive", () => {
|
|
|
113
150
|
"rm -rf C:\\",
|
|
114
151
|
'fs.rmSync("/", { recursive: true, force: true })',
|
|
115
152
|
'rimraf("/work")',
|
|
153
|
+
"find / -delete",
|
|
154
|
+
"find /home -type f -delete",
|
|
116
155
|
]) {
|
|
117
156
|
expect(classify(command), command).toContain("system.destructive");
|
|
118
157
|
}
|
|
@@ -375,6 +375,66 @@ const recursiveForceRms = (command: string): CommandSpan[] =>
|
|
|
375
375
|
.filter((invocation) => invocation.recursive && invocation.force)
|
|
376
376
|
.map((invocation) => invocation.span);
|
|
377
377
|
|
|
378
|
+
// The same tree delete spelled without `rm -rf`: `find` deleting what it walks, or handing each hit to `rm`, and a list
|
|
379
|
+
// piped into `rm` through xargs. An agent told `rm -rf` needs a card reaches for exactly these.
|
|
380
|
+
const FIND_INVOCATION = /\bfind\s+([^|;&\n]*)/g;
|
|
381
|
+
const FIND_DELETES = /\s(?:-delete\b|-exec(?:dir)?\s+(?:[^\s|;&]*\/)?rm\b)/;
|
|
382
|
+
const XARGS_RM = /\bxargs\b[^|;&\n]*?\s(?:[^\s|;&]*\/)?rm\b/g;
|
|
383
|
+
|
|
384
|
+
interface FindDelete {
|
|
385
|
+
// The paths find starts walking from: the words before its first expression (`-name`, `(`, `!`).
|
|
386
|
+
readonly starts: readonly string[];
|
|
387
|
+
readonly span: CommandSpan;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const parseFindDeletes = (command: string): FindDelete[] =>
|
|
391
|
+
[...command.matchAll(FIND_INVOCATION)].flatMap((invocation) => {
|
|
392
|
+
const args = invocation[1] as string;
|
|
393
|
+
if (!FIND_DELETES.test(` ${args}`)) {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
const words = args.trim().split(/\s+/);
|
|
397
|
+
const expression = words.findIndex((word) => /^[-(!]/.test(word));
|
|
398
|
+
const starts = (expression === -1 ? words : words.slice(0, expression)).filter((word) => word !== "").map(unquote);
|
|
399
|
+
return [{ starts, span: { start: invocation.index, end: invocation.index + invocation[0].trimEnd().length } }];
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const findAndXargsDeletes = (command: string): CommandSpan[] => [
|
|
403
|
+
...parseFindDeletes(command).map((found) => found.span),
|
|
404
|
+
...spansOf([XARGS_RM], command),
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
// What cuts a checkout off from its repository's history: deleting its `.git` (for a worktree, a one-line pointer
|
|
408
|
+
// whose loss strands every commit the checkout was reading), or `git init` over the folder it stands in, which makes
|
|
409
|
+
// a standalone repository sharing nothing with the one it came from. A `git init <new-dir>` for a new project is not
|
|
410
|
+
// this, so only an init of the current folder counts.
|
|
411
|
+
const GIT_POINTER = /(?:^|[/\\])\.git[/\\]?$/;
|
|
412
|
+
const GIT_INIT = /\bgit\s+init\b([^|;&\n]*)/g;
|
|
413
|
+
const GIT_INIT_VALUED = /^(?:-b|--initial-branch|--template|--separate-git-dir|--object-format|--ref-format)$/;
|
|
414
|
+
|
|
415
|
+
const initsHere = (args: string): boolean => {
|
|
416
|
+
const words = args.trim().split(/\s+/).filter((word) => word !== "");
|
|
417
|
+
const operands: string[] = [];
|
|
418
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
419
|
+
const word = words[index] as string;
|
|
420
|
+
if (GIT_INIT_VALUED.test(word)) {
|
|
421
|
+
index += 1;
|
|
422
|
+
} else if (!word.startsWith("-")) {
|
|
423
|
+
operands.push(unquote(word));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return operands.every((operand) => operand === "." || operand === "./");
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const checkoutUnlinks = (command: string): CommandSpan[] => [
|
|
430
|
+
...parseRm(command)
|
|
431
|
+
.filter((invocation) => invocation.operands.some((operand) => GIT_POINTER.test(operand)))
|
|
432
|
+
.map((invocation) => invocation.span),
|
|
433
|
+
...[...command.matchAll(GIT_INIT)]
|
|
434
|
+
.filter((invocation) => initsHere(invocation[1] as string))
|
|
435
|
+
.map((invocation) => ({ start: invocation.index, end: invocation.index + invocation[0].trimEnd().length })),
|
|
436
|
+
];
|
|
437
|
+
|
|
378
438
|
// A recursive delete aimed at a root, in either spelling the gate is handed (shell or script). Which targets count as
|
|
379
439
|
// roots is the locus's answer.
|
|
380
440
|
const rootDeletes = (program: string, locus: CommandLocus): CommandSpan[] => [
|
|
@@ -384,6 +444,9 @@ const rootDeletes = (program: string, locus: CommandLocus): CommandSpan[] => [
|
|
|
384
444
|
...nodeDeleteTargets(program)
|
|
385
445
|
.filter((delete_) => isRootTarget(delete_.target, locus))
|
|
386
446
|
.map((delete_) => delete_.span),
|
|
447
|
+
...parseFindDeletes(program)
|
|
448
|
+
.filter((found) => found.starts.some((start) => isRootTarget(start, locus)))
|
|
449
|
+
.map((found) => found.span),
|
|
387
450
|
];
|
|
388
451
|
|
|
389
452
|
// The g twins, built once at load: a card is minted per command held, classify runs per command typed.
|
|
@@ -434,10 +497,10 @@ const credentialReads = (command: string, context: CommandContext): CommandSpan[
|
|
|
434
497
|
|
|
435
498
|
// One function per class; empty means the command is not in it, so membership and evidence are the same walk.
|
|
436
499
|
const MATCHES: Readonly<Record<CommandClass, (command: string, context: CommandContext) => CommandSpan[]>> = {
|
|
437
|
-
"git.destructive": (command) => spansOf(GIT_DESTRUCTIVE_G, command),
|
|
500
|
+
"git.destructive": (command) => [...spansOf(GIT_DESTRUCTIVE_G, command), ...checkoutUnlinks(command)],
|
|
438
501
|
// The only class that can answer "not here": in its own copy a conversation's checkout is its own to move.
|
|
439
502
|
"git.branch-switch": (command, context) => (context.ownCheckout === true ? [] : spansOf(GIT_BRANCH_SWITCH_G, command)),
|
|
440
|
-
"files.destructive": (command) => [...recursiveForceRms(command), ...recursiveDeletes(command)],
|
|
503
|
+
"files.destructive": (command) => [...recursiveForceRms(command), ...recursiveDeletes(command), ...findAndXargsDeletes(command)],
|
|
441
504
|
"system.destructive": (command, context) => [...spansOf(BLOCK_DEVICE_G, command), ...rootDeletes(command, context.locus)],
|
|
442
505
|
"container.state": (command) => spansOf(CONTAINER_STATE_G, command),
|
|
443
506
|
"secrets.access": credentialReads,
|
|
@@ -489,6 +552,8 @@ export const COMMAND_CLASS_PATTERNS: Readonly<Record<CommandClass, readonly Comm
|
|
|
489
552
|
{ code: "git clean -f" },
|
|
490
553
|
{ code: "git branch -D" },
|
|
491
554
|
{ code: "git filter-branch" },
|
|
555
|
+
{ code: "rm .git", qualifier: "any flags: a checkout's link to its history" },
|
|
556
|
+
{ code: "git init", qualifier: "only in the current folder, which may already be a checkout" },
|
|
492
557
|
],
|
|
493
558
|
"git.branch-switch": [
|
|
494
559
|
{ code: "git switch <branch>" },
|
|
@@ -498,6 +563,8 @@ export const COMMAND_CLASS_PATTERNS: Readonly<Record<CommandClass, readonly Comm
|
|
|
498
563
|
{ code: "rm -rf <path>" },
|
|
499
564
|
{ code: "fs.rm(<path>, { recursive: true })", qualifier: "also rmSync, rmdir, rmdirSync" },
|
|
500
565
|
{ code: "rimraf(<path>)" },
|
|
566
|
+
{ code: "find <path> -delete", qualifier: "also -exec rm and -execdir rm" },
|
|
567
|
+
{ code: "xargs rm" },
|
|
501
568
|
],
|
|
502
569
|
"system.destructive": [
|
|
503
570
|
{ code: "mkfs" },
|
|
@@ -507,7 +574,7 @@ export const COMMAND_CLASS_PATTERNS: Readonly<Record<CommandClass, readonly Comm
|
|
|
507
574
|
{ code: "dd of=/dev/…" },
|
|
508
575
|
{ code: "shred /dev/…" },
|
|
509
576
|
{ code: "> /dev/sda" },
|
|
510
|
-
{ code: "rm -rf /", qualifier: "only when the target is a root, listed below" },
|
|
577
|
+
{ code: "rm -rf /", qualifier: "also find / -delete; only when the target is a root, listed below" },
|
|
511
578
|
],
|
|
512
579
|
"container.state": [
|
|
513
580
|
{ code: "docker volume rm", qualifier: "also remove, prune, and podman for any of these" },
|
|
@@ -64,6 +64,15 @@ export const COMMAND_RULE_CATALOG: readonly CommandRule[] = CommandClassSchema.o
|
|
|
64
64
|
export const CommandJudgeModeSchema = z.enum(["off", "watch", "on"]);
|
|
65
65
|
export type CommandJudgeMode = z.infer<typeof CommandJudgeModeSchema>;
|
|
66
66
|
|
|
67
|
+
// What an agent's own project-dependency install (`pnpm add`, `npm install`, `uv sync`) does, one answer for every
|
|
68
|
+
// turn. Where the install lands is the sandbox's business, not the owner's: an isolated turn installs into its own copy
|
|
69
|
+
// and a main-tree turn takes the install lane the daemon's own installs take. This only says whether a person is asked:
|
|
70
|
+
// - automatic: it runs
|
|
71
|
+
// - ask: a card in the turn; the turn waits on that one call, and "allow for this conversation" stops the asking
|
|
72
|
+
// - never: refused; the manifest change still lands and the daemon installs it then
|
|
73
|
+
export const ProjectInstallModeSchema = z.enum(["automatic", "ask", "never"]);
|
|
74
|
+
export type ProjectInstallMode = z.infer<typeof ProjectInstallModeSchema>;
|
|
75
|
+
|
|
67
76
|
// What the judge answers, one instruction per verdict:
|
|
68
77
|
// - allow: run it, say nothing, the ordinary answer for a triage false positive
|
|
69
78
|
// - ask: raise the card; the only path to a person, meant to be rare
|
|
@@ -122,6 +131,7 @@ Ask me before:
|
|
|
122
131
|
|
|
123
132
|
- publishing or releasing anything (npm publish, a GitHub release, a container push);
|
|
124
133
|
- force-pushing, hard-resetting or otherwise discarding commits that are not this turn's own work;
|
|
134
|
+
- deleting a checkout's \`.git\` or running \`git init\` in a folder that already is one: in a worktree that cuts it off from my workspace's history, and its work can no longer come back to me;
|
|
125
135
|
- moving a checkout onto another branch (\`git checkout <branch>\`, \`git switch\`) — you only ever see this for a tree that is mine as well as yours, never for a copy of your own, so the worktree paragraph above does not cover it;
|
|
126
136
|
- sending a credential anywhere outside this container.
|
|
127
137
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { hostAllowed, hostMatches, normalizeHost, normalizeHostPattern, tightensHostGuard } from "./secret-hosts.js";
|
|
2
|
+
|
|
3
|
+
// The pattern a person writes against the host a use would reach. Every boundary a limit turns on is pinned by value:
|
|
4
|
+
// the apex a wildcard leaves out, the look-alike that only shares a suffix, and which edits take hosts away.
|
|
5
|
+
|
|
6
|
+
describe("normalizeHostPattern", () => {
|
|
7
|
+
it("reads what a person types or pastes as the host it means", () => {
|
|
8
|
+
expect(normalizeHostPattern(" API.GitHub.com. ")).toBe("api.github.com");
|
|
9
|
+
expect(normalizeHostPattern("https://api.github.com/user?per_page=5")).toBe("api.github.com");
|
|
10
|
+
expect(normalizeHostPattern("api.github.com:443")).toBe("api.github.com");
|
|
11
|
+
expect(normalizeHostPattern("*.githubusercontent.com")).toBe("*.githubusercontent.com");
|
|
12
|
+
expect(normalizeHostPattern("localhost")).toBe("localhost");
|
|
13
|
+
expect(normalizeHostPattern("10.0.0.2")).toBe("10.0.0.2");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("refuses what is not one host or one domain's hosts", () => {
|
|
17
|
+
expect(normalizeHostPattern("*")).toBeUndefined();
|
|
18
|
+
expect(normalizeHostPattern("*.com")).toBeUndefined();
|
|
19
|
+
expect(normalizeHostPattern("api.*.com")).toBeUndefined();
|
|
20
|
+
expect(normalizeHostPattern("api.github.com/user")).toBeUndefined();
|
|
21
|
+
expect(normalizeHostPattern("user@api.github.com")).toBeUndefined();
|
|
22
|
+
expect(normalizeHostPattern("-bad.example.com")).toBeUndefined();
|
|
23
|
+
expect(normalizeHostPattern("")).toBeUndefined();
|
|
24
|
+
expect(normalizeHostPattern(`${"a".repeat(64)}.com`)).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("normalizeHost", () => {
|
|
29
|
+
it("takes a plain host or address and nothing shaped like a pattern or a stray token", () => {
|
|
30
|
+
expect(normalizeHost("API.github.com.")).toBe("api.github.com");
|
|
31
|
+
expect(normalizeHost("*.github.com")).toBeUndefined();
|
|
32
|
+
expect(normalizeHost("[::1]")).toBeUndefined();
|
|
33
|
+
expect(normalizeHost("api.github.com\u0001")).toBeUndefined();
|
|
34
|
+
expect(normalizeHost("$host")).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("hostMatches", () => {
|
|
39
|
+
it("matches an exact host only to itself", () => {
|
|
40
|
+
expect(hostMatches("api.github.com", "api.github.com")).toBe(true);
|
|
41
|
+
expect(hostMatches("api.github.com", "github.com")).toBe(false);
|
|
42
|
+
expect(hostMatches("github.com", "api.github.com")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("matches a wildcard to every host under the domain, at any depth, and not to the domain itself", () => {
|
|
46
|
+
expect(hostMatches("*.github.com", "api.github.com")).toBe(true);
|
|
47
|
+
expect(hostMatches("*.github.com", "a.b.github.com")).toBe(true);
|
|
48
|
+
expect(hostMatches("*.github.com", "github.com")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not match a host that merely ends in the same letters", () => {
|
|
52
|
+
expect(hostMatches("*.github.com", "evilgithub.com")).toBe(false);
|
|
53
|
+
expect(hostMatches("*.github.com", "github.com.evil.example")).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("allows a host any one pattern in the list matches", () => {
|
|
57
|
+
expect(hostAllowed(["api.github.com", "*.githubusercontent.com"], "raw.githubusercontent.com")).toBe(true);
|
|
58
|
+
expect(hostAllowed(["api.github.com", "*.githubusercontent.com"], "evil.example")).toBe(false);
|
|
59
|
+
expect(hostAllowed([], "api.github.com")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("tightensHostGuard", () => {
|
|
64
|
+
const on = (...hosts: string[]) => ({ guard: true, hosts });
|
|
65
|
+
const off = (...hosts: string[]) => ({ guard: false, hosts });
|
|
66
|
+
|
|
67
|
+
it("reads turning the guard on as tightening, whatever the list, and turning it off as loosening", () => {
|
|
68
|
+
expect(tightensHostGuard(undefined, on("api.github.com"))).toBe(true);
|
|
69
|
+
expect(tightensHostGuard(off("api.github.com"), on())).toBe(true);
|
|
70
|
+
expect(tightensHostGuard(on("api.github.com"), off("api.github.com"))).toBe(false);
|
|
71
|
+
expect(tightensHostGuard(undefined, off())).toBe(true);
|
|
72
|
+
expect(tightensHostGuard(off(), off("evil.example"))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("reads taking a host off a guarded list as tightening, down to none, and adding one as loosening", () => {
|
|
76
|
+
expect(tightensHostGuard(on("api.github.com", "github.com"), on("github.com"))).toBe(true);
|
|
77
|
+
expect(tightensHostGuard(on("api.github.com"), on())).toBe(true);
|
|
78
|
+
expect(tightensHostGuard(on("api.github.com"), on("api.github.com", "evil.example"))).toBe(false);
|
|
79
|
+
expect(tightensHostGuard(on(), on("api.github.com"))).toBe(false);
|
|
80
|
+
expect(tightensHostGuard(on("api.github.com"), on("api.github.com"))).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("counts a host or a deeper wildcard under a held wildcard as covered, and the domain itself as not", () => {
|
|
84
|
+
expect(tightensHostGuard(on("*.github.com"), on("api.github.com"))).toBe(true);
|
|
85
|
+
expect(tightensHostGuard(on("*.github.com"), on("*.api.github.com"))).toBe(true);
|
|
86
|
+
expect(tightensHostGuard(on("*.github.com"), on("github.com"))).toBe(false);
|
|
87
|
+
expect(tightensHostGuard(on("api.github.com"), on("*.api.github.com"))).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Which hosts a stored secret may be sent to: an owner's list per secret, matched against the hosts one use would reach.
|
|
2
|
+
// Shared by the daemon's check, its routes and the Secrets view, so the three cannot disagree on what a pattern means.
|
|
3
|
+
// A list names hosts, never ports or paths: the question it answers is who receives the value, not which door.
|
|
4
|
+
|
|
5
|
+
// One DNS label: letters, digits and inner hyphens, at most 63 characters.
|
|
6
|
+
const LABEL = String.raw`[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?`;
|
|
7
|
+
|
|
8
|
+
// An exact host (`api.github.com`, `localhost`, `10.0.0.2`) or every host under a domain (`*.github.com`). A wildcard
|
|
9
|
+
// names at least two labels under it, so `*.com` cannot stand in for most of the internet.
|
|
10
|
+
export const SECRET_HOST_PATTERN_RE = new RegExp(String.raw`^(?:\*\.${LABEL}(?:\.${LABEL})+|${LABEL}(?:\.${LABEL})*)$`);
|
|
11
|
+
export const SECRET_HOST_MAX = 253;
|
|
12
|
+
// Enough for a service's API, uploads and content hosts several times over; a list longer than this is no limit.
|
|
13
|
+
export const SECRET_HOSTS_MAX = 64;
|
|
14
|
+
|
|
15
|
+
const WILDCARD = "*.";
|
|
16
|
+
|
|
17
|
+
// What a person types or pastes, as the pattern it means: lowercased, trimmed, a trailing dot and a port dropped, and a
|
|
18
|
+
// whole URL reduced to its host. Undefined for anything that is not a host pattern, so a caller refuses it by name.
|
|
19
|
+
export const normalizeHostPattern = (raw: string): string | undefined => {
|
|
20
|
+
let text = raw.trim().toLowerCase();
|
|
21
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//.test(text)) {
|
|
22
|
+
try {
|
|
23
|
+
text = new URL(text).hostname;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
text = text.replace(/:\d+$/, "").replace(/\.$/, "");
|
|
29
|
+
return text.length <= SECRET_HOST_MAX && SECRET_HOST_PATTERN_RE.test(text) ? text : undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// A host a use would reach, in the form patterns are written in: lowercased, one trailing dot dropped. Undefined when it
|
|
33
|
+
// is not a plain host name or IPv4 address, which callers read as "cannot tell where this goes".
|
|
34
|
+
export const normalizeHost = (raw: string): string | undefined => {
|
|
35
|
+
const host = raw.toLowerCase().replace(/\.$/, "");
|
|
36
|
+
return host.length <= SECRET_HOST_MAX && !host.startsWith(WILDCARD) && SECRET_HOST_PATTERN_RE.test(host) ? host : undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// `*.github.com` matches every host under github.com at any depth, and not github.com itself; anything else matches only
|
|
40
|
+
// itself. Both sides are expected normalized.
|
|
41
|
+
export const hostMatches = (pattern: string, host: string): boolean =>
|
|
42
|
+
pattern.startsWith(WILDCARD) ? host.endsWith(pattern.slice(1)) : host === pattern;
|
|
43
|
+
|
|
44
|
+
export const hostAllowed = (patterns: readonly string[], host: string): boolean => patterns.some((pattern) => hostMatches(pattern, host));
|
|
45
|
+
|
|
46
|
+
// Whether every host `pattern` matches is already matched by `patterns`: an exact host by any pattern that matches it, a
|
|
47
|
+
// wildcard only by itself or a wildcard over a domain above it.
|
|
48
|
+
const covered = (patterns: readonly string[], pattern: string): boolean => {
|
|
49
|
+
if (!pattern.startsWith(WILDCARD)) {
|
|
50
|
+
return hostAllowed(patterns, pattern);
|
|
51
|
+
}
|
|
52
|
+
const suffix = pattern.slice(1);
|
|
53
|
+
return patterns.some((held) => held.startsWith(WILDCARD) && suffix.endsWith(held.slice(1)));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// One secret's host guard as a change reads it: whether it asks off the list, and the list. Absent reads as off.
|
|
57
|
+
export interface HostGuardSetting {
|
|
58
|
+
readonly guard: boolean;
|
|
59
|
+
readonly hosts: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Whether going from `current` to `next` only ever makes the agent ask more: turning the guard on, or taking hosts off a
|
|
63
|
+
// guarded list (down to none, where every use asks). The one test for who may make a change: anybody who may use
|
|
64
|
+
// secrets may tighten a guard, only the owner may loosen one, by adding a host or turning it off.
|
|
65
|
+
export const tightensHostGuard = (current: HostGuardSetting | undefined, next: HostGuardSetting): boolean => {
|
|
66
|
+
if (!next.guard) {
|
|
67
|
+
return current === undefined || !current.guard;
|
|
68
|
+
}
|
|
69
|
+
if (current === undefined || !current.guard) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return next.hosts.every((pattern) => covered(current.hosts, pattern));
|
|
73
|
+
};
|
|
@@ -28,6 +28,9 @@ export const RAW_ROUTES = {
|
|
|
28
28
|
"POST /system/ws-ticket": { beforeBoot: true, floor: "collaborator", control: "never" },
|
|
29
29
|
// A WebSocket upgrade carries no Authorization header; the terminal and both browser wires check a query ticket.
|
|
30
30
|
"GET /system/terminal": { auth: "door", beforeBoot: true, control: "never", front: true },
|
|
31
|
+
// The front's proof of life (vitals.ts): no credential and nothing of the workspace, readable by any origin, answered
|
|
32
|
+
// whatever state the daemon is in.
|
|
33
|
+
"GET /system/vitals": { auth: "door", beforeBoot: true, front: true },
|
|
31
34
|
// Desktop sync's byte pipe, guarded again by sshd's own key check against the same enrollment.
|
|
32
35
|
"GET /system/sync/ssh": { sync: "pipe", control: "never", lane: "bulk" },
|
|
33
36
|
"GET /system/browser-profile": { auth: "door", beforeBoot: true, control: "never" },
|
|
@@ -61,6 +64,9 @@ export const RAW_ROUTES = {
|
|
|
61
64
|
"POST /environment/approve": {},
|
|
62
65
|
"POST /environment/reject": {},
|
|
63
66
|
"POST /environment/runtime-install": {},
|
|
67
|
+
"POST /environment/remove": {},
|
|
68
|
+
"POST /environment/rebuild-when-idle": {},
|
|
69
|
+
"DELETE /environment/rebuild-when-idle": {},
|
|
64
70
|
"GET /engines": {},
|
|
65
71
|
"POST /engines/channel": {},
|
|
66
72
|
"POST /engines/update": {},
|
|
@@ -92,13 +98,14 @@ export const RAW_ROUTES = {
|
|
|
92
98
|
"GET /wallet/status": { agent: true, control: "never" },
|
|
93
99
|
"POST /wallet/fetch": { agent: true, control: "never" },
|
|
94
100
|
"GET /wallet/history": { agent: true, control: "never" },
|
|
95
|
-
// The `agents` CLI's children: start, follow up, answer (never a consent card), cancel, list.
|
|
101
|
+
// The `agents` CLI's children: start, follow up, answer (never a consent card), cancel, merge held work, list.
|
|
96
102
|
"POST /children/spawn": { agent: true, control: "never" },
|
|
97
103
|
"GET /children/providers": { agent: true, control: "never" },
|
|
98
104
|
"POST /children/wait": { agent: true, control: "never" },
|
|
99
105
|
"POST /children/send": { agent: true, control: "never" },
|
|
100
106
|
"POST /children/answer": { agent: true, control: "never" },
|
|
101
107
|
"POST /children/cancel": { agent: true, control: "never" },
|
|
108
|
+
"POST /children/merge": { agent: true, control: "never" },
|
|
102
109
|
"GET /children": { agent: true, control: "never" },
|
|
103
110
|
// The fleet reads, and the one write: words in front of another conversation, which a person can do by typing.
|
|
104
111
|
"GET /fleet": { agent: true },
|