@starci/hfs 4.8.0 → 4.9.1
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/CHANGELOG.md +10 -0
- package/emit/operation-schemas.mjs +3 -0
- package/lint/bound-sys.cjs +2 -2
- package/package.json +1 -1
- package/runtime/engine/admission.mjs +1 -1
- package/runtime/engine/config.mjs +2 -2
- package/runtime/engine/db/machine.mjs +2 -3
- package/runtime/engine/invalid-config.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
- package/runtime/knowledge/hfs/rules.yaml +23 -0
- package/runtime/modules/kernel/failure-codes.yaml +10 -0
- package/runtime/scripts/guards/git-policy.mjs +2 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
- package/runtime/scripts/lib/package-at.mjs +5 -6
- package/runtime/scripts/lib/roots.mjs +2 -2
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
- package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
- package/scaffold/app.mjs +4 -4
- package/src/main.mjs +3 -3
- package/sync/index.mjs +14 -1
- package/sync/managed.mjs +3 -2
- package/templates/app/codegen-graphql.mjs.partial +4 -4
- package/templates/app/quality-config/sonar-project.properties +4 -2
- package/runtime/engine/canonical-json.mjs +0 -14
- package/runtime/engine/db/ledger-open.mjs +0 -38
- package/runtime/engine/db/ledger-paths.mjs +0 -87
- package/runtime/engine/db/ledger.mjs +0 -1068
- package/runtime/engine/db/provision-authority.mjs +0 -76
- package/runtime/engine/db/provision-operations.mjs +0 -146
- package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
- package/runtime/scripts/agent/codex-app-server.mjs +0 -79
- package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
- package/runtime/scripts/agent/host-agents.mjs +0 -80
- package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
- package/runtime/scripts/agent/trust-launch.mjs +0 -191
- package/runtime/scripts/agent/trust.mjs +0 -523
- package/runtime/scripts/api/fs/temp-path.mjs +0 -8
- package/runtime/scripts/api/git/diff-names.mjs +0 -8
- package/runtime/scripts/api/git/head-time.mjs +0 -10
- package/runtime/scripts/api/git/index-lock.mjs +0 -78
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/runtime/scripts/api/git/merge-tree.mjs +0 -5
- package/runtime/scripts/api/git/rev-list.mjs +0 -5
- package/runtime/scripts/api/git/rev-parse.mjs +0 -13
- package/runtime/scripts/api/git/status-query.mjs +0 -5
- package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
- package/runtime/scripts/api/npm/lib.mjs +0 -32
- package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
- package/runtime/scripts/api/npm/run-npm.mjs +0 -6
- package/runtime/scripts/api/npm/view.mjs +0 -30
- package/runtime/scripts/api/npm/whoami.mjs +0 -8
- package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
- package/runtime/scripts/api/process/kill-tree.mjs +0 -22
- package/runtime/scripts/api/process/lib.mjs +0 -125
- package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
- package/runtime/scripts/api/process/process-list.mjs +0 -28
- package/runtime/scripts/api/process/system-tool.mjs +0 -37
- package/runtime/scripts/gates/canon-digest.mjs +0 -81
- package/runtime/scripts/gates/canon-pins.mjs +0 -119
- package/runtime/scripts/gates/gate-input.mjs +0 -98
- package/runtime/scripts/gates/gate.mjs +0 -636
- package/runtime/scripts/gates/grammar-dist.mjs +0 -206
- package/runtime/scripts/gates/output.mjs +0 -40
- package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
- package/runtime/scripts/gates/test-world-run.mjs +0 -252
- package/runtime/scripts/gates/type-impact.mjs +0 -277
- package/runtime/scripts/gates/type-program.mjs +0 -113
- package/runtime/scripts/guards/assigned-command.mjs +0 -12
- package/runtime/scripts/guards/call-rights.mjs +0 -47
- package/runtime/scripts/guards/command-guard.mjs +0 -468
- package/runtime/scripts/guards/command-policy.mjs +0 -342
- package/runtime/scripts/guards/critic-reach.mjs +0 -82
- package/runtime/scripts/guards/deps-guard.mjs +0 -150
- package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
- package/runtime/scripts/guards/guards-root.mjs +0 -27
- package/runtime/scripts/guards/hook-install.mjs +0 -343
- package/runtime/scripts/guards/hook-verdict.mjs +0 -12
- package/runtime/scripts/guards/install-verdict.mjs +0 -41
- package/runtime/scripts/guards/kernel-seat.mjs +0 -63
- package/runtime/scripts/guards/launch-verdict.mjs +0 -74
- package/runtime/scripts/guards/op-starci.mjs +0 -16
- package/runtime/scripts/guards/op-write-scope.mjs +0 -27
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
- package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
- package/runtime/scripts/guards/protected-zone.mjs +0 -56
- package/runtime/scripts/guards/refusals.mjs +0 -27
- package/runtime/scripts/guards/rights.mjs +0 -268
- package/runtime/scripts/guards/seat-tools.mjs +0 -47
- package/runtime/scripts/guards/shell-commands.mjs +0 -328
- package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
- package/runtime/scripts/guards/verdict-process.mjs +0 -81
- package/runtime/scripts/lib/app-data.mjs +0 -11
- package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
- package/runtime/scripts/lib/check-format.mjs +0 -7
- package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
- package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
- package/runtime/scripts/lib/git-dir.mjs +0 -120
- package/runtime/scripts/lib/guard-command.mjs +0 -24
- package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
- package/runtime/scripts/lib/json-formats.mjs +0 -34
- package/runtime/scripts/lib/managed-json.mjs +0 -51
- package/runtime/scripts/lib/menu-parts.mjs +0 -11
- package/runtime/scripts/lib/op-gate.mjs +0 -15
- package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
- package/runtime/scripts/lib/release-version.mjs +0 -134
- package/runtime/scripts/lib/runtime-location.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
- package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
- package/runtime/scripts/lib/seat-env.mjs +0 -43
- package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
- package/runtime/scripts/lib/source-package-imports.mjs +0 -111
- package/runtime/scripts/lib/tar-files.mjs +0 -39
- package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
- package/runtime/scripts/lib/worktree-record.mjs +0 -14
- package/runtime/scripts/machine/ask-receipts.mjs +0 -113
- package/runtime/scripts/machine/evidence-store.mjs +0 -223
- package/runtime/scripts/machine/home.mjs +0 -216
- package/runtime/scripts/machine/host-lock.mjs +0 -184
- package/runtime/scripts/machine/lock-recovery.mjs +0 -55
- package/runtime/scripts/machine/roles-contract.mjs +0 -90
- package/runtime/scripts/machine/runtime-change.mjs +0 -20
- package/runtime/scripts/machine/workflow-tree.mjs +0 -122
- package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
- package/runtime/scripts/reconciler/guard-row.mjs +0 -25
- package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
- package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
- package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
- package/runtime/scripts/work/brand/brand.mjs +0 -892
- package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
- package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
- package/runtime/scripts/work/draw/draw-source.mjs +0 -412
- package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
- package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
- package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
- package/runtime/scripts/work/draw-grammar.mjs +0 -175
- package/runtime/scripts/work/draw-render-page.mjs +0 -188
- package/runtime/scripts/work/draw-render.mjs +0 -626
- package/runtime/scripts/work/png.mjs +0 -237
- package/runtime/scripts/work/render-tools.mjs +0 -64
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// The refusals of launches outside the runtime (raw terminal create, headless agent CLIs, an agent's own Orca worktree, a test
|
|
2
|
-
// world in a uat op), split out of command-guard.mjs.
|
|
3
|
-
// An agent launch outside Orca is invisible to it: no liveness, no stop, no release, and a dead worker sits unnoticed
|
|
4
|
-
// (owner rule 2026-10-01). Every agent and every worker terminal starts through `orca orchestration worker-start`.
|
|
5
|
-
const WORKER_START = '`orca orchestration worker-start --agent <provider> [--model <id>] --worktree <selector> --spec "<task>" --task-title "<title>"`, supervised with worker-show / worker-read / worker-stop / worker-release';
|
|
6
|
-
// The first non-flag word of `args`, skipping the values of `valued` flags.
|
|
7
|
-
const subcommandOf = (args, valued) => {
|
|
8
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
9
|
-
const a = args[i];
|
|
10
|
-
if (a === '--') return args[i + 1] ?? null;
|
|
11
|
-
if (a.startsWith('-')) {
|
|
12
|
-
if (valued.test(a)) { i += 1; }
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
return a;
|
|
16
|
-
}
|
|
17
|
-
return null;
|
|
18
|
-
};
|
|
19
|
-
const CODEX_VALUED = /^(?:-m|--model|-c|--config|-C|--cd|-s|--sandbox|-a|--ask-for-approval|-p|--profile|-i|--image|--enable|--disable|--add-dir|--local-provider)$/;
|
|
20
|
-
const printFlag = (args, flags) => {
|
|
21
|
-
const options = args.includes('--') ? args.slice(0, args.indexOf('--')) : args;
|
|
22
|
-
return options.find((a) => flags.includes(a)) ?? null;
|
|
23
|
-
};
|
|
24
|
-
// The one headless call the runtime makes for an op (tiers.yaml calls.imagegen; owner decision 2026-10-07).
|
|
25
|
-
const IMAGE_CALL = ' An image is generated with `starci work imagegen` (interface.draw, interface.asset and brand.decide), never with a hand-run codex exec.';
|
|
26
|
-
const launchRefusal = (program, args, how) => ({ command: [program, ...args].join(' ').slice(0, 200),
|
|
27
|
-
reason: `${how} outside Orca's supervision: no liveness, no stop or release, and a dead worker goes unnoticed (owner rule 2026-10-01)`,
|
|
28
|
-
remedy: `launch every agent and worker through ${WORKER_START}${program === 'codex' ? IMAGE_CALL : ''}` });
|
|
29
|
-
// The workspace and runtime rules (workspace.manage, runtime.operate; OPS2 3.1): an agent never creates or removes an Orca
|
|
30
|
-
// worktree itself - the runtime's worktree API (scripts/machine/worktree-orca.mjs createOrcaWorktree / removeOrcaWorktree,
|
|
31
|
-
// releaseWorkflowWorktree) runs the link check first and keeps the registry and its cap (git worktree writes are
|
|
32
|
-
// git-policy.mjs WORKTREE_NOT_OPS).
|
|
33
|
-
const ORCA_WORKTREE_WRITES = new Set(['create', 'add', 'rm', 'remove', 'delete', 'prune', 'move']);
|
|
34
|
-
// The uat rule (uat.verify, uat.assisted.*; OPS2 3.1): a walk runs on the app's real dev stack, never a test world.
|
|
35
|
-
const TEST_WORLD_RUNNER = /(?:^|[\\/])test-world-run\.mjs$/;
|
|
36
|
-
const testWorldOf = (program, args) => {
|
|
37
|
-
if (program === 'node' && args.some((a) => TEST_WORLD_RUNNER.test(a))) return 'test-world-run.mjs';
|
|
38
|
-
if (program !== 'starci') return null;
|
|
39
|
-
const words = args.filter((a) => !a.startsWith('-'));
|
|
40
|
-
return words[0] === 'gate' && words[1] === 'test-world' ? 'starci gate test-world' : null;
|
|
41
|
-
};
|
|
42
|
-
const headlessHow = (program, args) => {
|
|
43
|
-
if (program === 'codex') {
|
|
44
|
-
const sub = subcommandOf(args, CODEX_VALUED);
|
|
45
|
-
return sub === 'exec' || sub === 'e' ? `codex ${sub} runs a headless Codex agent` : null;
|
|
46
|
-
}
|
|
47
|
-
if (program === 'claude' || program === 'cursor-agent' || program === 'devin') {
|
|
48
|
-
const flag = printFlag(args, ['-p', '--print']);
|
|
49
|
-
return flag ? `${program} ${flag} runs a headless agent` : null;
|
|
50
|
-
}
|
|
51
|
-
if (program === 'gemini') {
|
|
52
|
-
const flag = printFlag(args, ['-p', '--prompt']);
|
|
53
|
-
return flag ? `gemini ${flag} runs a headless agent` : null;
|
|
54
|
-
}
|
|
55
|
-
if (program === 'opencode' && subcommandOf(args, /^(?:-m|--model|--agent|--log-level)$/) === 'run') return 'opencode run runs a headless agent';
|
|
56
|
-
return null;
|
|
57
|
-
};
|
|
58
|
-
export const launchVerdict = (program, args, guard = null) => {
|
|
59
|
-
if (program === 'orca') {
|
|
60
|
-
const words = args.filter((a) => !a.startsWith('-'));
|
|
61
|
-
if (words[0] === 'terminal' && words[1] === 'create') return { code: 'RAW_TERMINAL_CREATE', ...launchRefusal(program, args, 'a raw terminal create starts a terminal outside worker-start') };
|
|
62
|
-
if (words[0] === 'worktree' && ORCA_WORKTREE_WRITES.has(words[1])) return { code: 'AGENT_ORCA_WORKTREE', command: [program, ...args].join(' ').slice(0, 200),
|
|
63
|
-
reason: `orca worktree ${words[1]}: an agent never creates or removes a worktree - a removal without the runtime's link check follows node_modules junctions into the live tree (inc-c8fbf76aa499; Orca itself walks junctions), and a tree outside the registry escapes its cap and GC`,
|
|
64
|
-
remedy: 'inspect with `starci machine worktrees counts`; remove clean merged lane worktrees with `starci machine worktrees-clean`; worktrees are created and removed only by the runtime worktree API (scripts/machine/worktree-orca.mjs createOrcaWorktree / removeOrcaWorktree, scripts/kernel/workflow-worktree.mjs releaseWorkflowWorktree); report a need you cannot meet as blocked environment' };
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
const op = String(guard?.op ?? '');
|
|
68
|
-
const testWorld = op.startsWith('uat.') ? testWorldOf(program, args) : null;
|
|
69
|
-
if (testWorld) return { code: 'UAT_TEST_WORLD', command: [program, ...args].join(' ').slice(0, 200),
|
|
70
|
-
reason: `${op} walks the app's real dev stack: a test world (${testWorld}) fakes what the walk must prove`,
|
|
71
|
-
remedy: 'bring up the dev stack the app-root .starcistacks/<env> declares with its own start commands, check it with env-health, and walk it; a stack that will not come up is reported blocked environment' };
|
|
72
|
-
const how = headlessHow(program, args);
|
|
73
|
-
return how ? { code: 'AGENT_HEADLESS_LAUNCH', ...launchRefusal(program, args, how) } : null;
|
|
74
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// op-starci.mjs — the starci groups an Op never runs, from the `op` table of modules/kernel/command-policy.yaml. An Op's starci calls are
|
|
2
|
-
// its work: the capability verbs for servers, renders, image generation and tests, the checks, and its report to its Kernel. The control
|
|
3
|
-
// plane (the Supervisor, the reconciler, workflow start and routing, Debug) belongs to the controllers, the Kernel and the Supervisor.
|
|
4
|
-
import { refusal } from './rights.mjs';
|
|
5
|
-
|
|
6
|
-
/** The first non-option word of a starci call: its group. */
|
|
7
|
-
const groupOf = (args) => String(args.find((word) => !String(word).startsWith('-')) ?? '');
|
|
8
|
-
|
|
9
|
-
/** The refusal of a starci call of a group an Op never runs, or null. */
|
|
10
|
-
export function opStarciVerdict({ policy, program, args, text }) {
|
|
11
|
-
const table = policy?.op;
|
|
12
|
-
if (!table || program !== 'starci') return null;
|
|
13
|
-
const group = groupOf(args);
|
|
14
|
-
if (!(table['deny-groups'] ?? []).includes(group)) return null;
|
|
15
|
-
return refusal('RIGHTS_OP_CONTROL_PLANE', text, `starci ${group} is the control plane: an Op runs the capability verbs of its work and reports to its Kernel`, table.use, table.use);
|
|
16
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// op-write-scope.mjs — where an Op may write: its attempt's owned paths and the runtime temp directory, nothing else. The owned paths
|
|
2
|
-
// are the absolute paths its job guard records at launch (scripts/guards/hook-install.mjs writeJobGuard); a file the Op needs outside
|
|
3
|
-
// them is reported as owed, never written. The PreToolUse guard applies it to Edit, Write and the shell write targets alike
|
|
4
|
-
// (rights.mjs fileWriteVerdict).
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import { tempRoot } from '../../engine/temp-root.mjs';
|
|
7
|
-
import { pathKey, sameOrUnder } from '../lib/path-key.mjs';
|
|
8
|
-
|
|
9
|
-
const inside = (file, dirs) => dirs.some((dir) => sameOrUnder(pathKey(file), pathKey(dir)));
|
|
10
|
-
|
|
11
|
-
/** Directories every Op may write: the runtime temp root and the OS temp directory. */
|
|
12
|
-
const scratchDirs = (env) => [tempRoot({ env }), os.tmpdir(), env?.TEMP, env?.TMP, env?.TMPDIR].filter(Boolean);
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The refusal fields {code, reason, remedy} for an Op write to `file`, or null when it may write there. An Op with no workflow
|
|
16
|
-
* worktree in its guard (a guard without scope information) is not confined.
|
|
17
|
-
*/
|
|
18
|
-
export function opWriteScopeRefusal({ file, guard, env = process.env }) {
|
|
19
|
-
if (!guard?.workflowWorktree) return null;
|
|
20
|
-
if (inside(file, guard.owned ?? [])) return null;
|
|
21
|
-
if (!inside(file, [guard.workflowWorktree]) && inside(file, scratchDirs(env))) return null;
|
|
22
|
-
return {
|
|
23
|
-
code: 'RIGHTS_OP_OUTSIDE_OWNED',
|
|
24
|
-
reason: 'an Op writes only inside the paths its attempt owns and the runtime temp directory; this file is outside them',
|
|
25
|
-
remedy: 'put scratch files in the runtime temp directory; a change the work needs outside your owned paths is reported as owed in your report (starci kernel report) for your Kernel to cut',
|
|
26
|
-
};
|
|
27
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// Own-seat lifecycle admission for R223; the policy table owns the verbs, roles, types and options.
|
|
2
|
-
const optionsOf = (args, entry) => {
|
|
3
|
-
const values = new Set(entry.values ?? []), flags = new Set(entry.flags ?? []);
|
|
4
|
-
const options = new Map();
|
|
5
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
6
|
-
const word = args[i], at = word.indexOf('=');
|
|
7
|
-
const name = at < 0 ? word : word.slice(0, at);
|
|
8
|
-
if (options.has(name)) return null; // Ambiguous identities/types never qualify, even if repeated identically.
|
|
9
|
-
if (flags.has(name) && at < 0) { options.set(name, true); continue; }
|
|
10
|
-
if (!values.has(name)) return null; // No positionals, terminators or unknown identity-changing options.
|
|
11
|
-
const value = at < 0 ? args[++i] : word.slice(at + 1);
|
|
12
|
-
if (value == null || (at < 0 && value.startsWith('--'))) return null;
|
|
13
|
-
options.set(name, value);
|
|
14
|
-
}
|
|
15
|
-
return options;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/** True only for a policy-listed lifecycle call whose explicit identity equals the trusted caller handle. */
|
|
19
|
-
export function orcaSelfLifecycleAllowed({ role, args, handle, policy }) {
|
|
20
|
-
const lifecycle = policy.orca?.['self-lifecycle'];
|
|
21
|
-
if (!handle || args[0] !== lifecycle?.group || !lifecycle.roles?.includes(role)) return false;
|
|
22
|
-
const entry = lifecycle.verbs?.[args[1]];
|
|
23
|
-
if (!entry || (entry.roles && !entry.roles.includes(role))) return false;
|
|
24
|
-
const options = optionsOf(args.slice(2), entry);
|
|
25
|
-
if (!options || options.get(entry.identity) !== handle) return false;
|
|
26
|
-
const types = entry.typesByRole?.[role] ?? entry.types;
|
|
27
|
-
return !types || types.includes(options.get('--type'));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The option a role may not use on a policy-listed lifecycle call ('refuse-for' in the policy entry), or null: an Op that names
|
|
32
|
-
* a recipient (--to) or another Run (--run) addresses someone other than its own Kernel.
|
|
33
|
-
*/
|
|
34
|
-
export function orcaAddressedOption({ role, args, policy }) {
|
|
35
|
-
const lifecycle = policy.orca?.['self-lifecycle'];
|
|
36
|
-
if (args[0] !== lifecycle?.group) return null;
|
|
37
|
-
const refused = lifecycle.verbs?.[args[1]]?.['refuse-for']?.[role] ?? [];
|
|
38
|
-
return args.slice(2).map((word) => String(word).split('=', 1)[0]).find((name) => refused.includes(name)) ?? null;
|
|
39
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// process-kill-verdict.mjs - the command guard's process rules: a kill that selects processes by image name or pattern
|
|
2
|
-
// (PROCESS_KILL_BY_NAME), and a kill whose targets come from a process QUERY - Get-CimInstance Win32_Process, Get-Process,
|
|
3
|
-
// ps | grep, pgrep - which ends other lanes' processes by their command line, parent or path (a lane killed other lanes'
|
|
4
|
-
// node test runs that way, 2026-10-02). An agent ends only a PID it started and recorded: `taskkill /PID <pid>`,
|
|
5
|
-
// `kill <pid>`, `Stop-Process -Id <pid>`, or a variable assigned from its own Start-Process or `cmd & pid=$!`.
|
|
6
|
-
// Reads (Get-CimInstance listing, `ps aux | grep node`) and text that only mentions these commands pass: the query rule
|
|
7
|
-
// judges parsed commands, so an echo, a commit message or a heredoc never reaches it.
|
|
8
|
-
// A kill whose variable was set in an EARLIER shell call (`$x = ...` in one call, `Stop-Process -Id $x` or `kill $pid` in the
|
|
9
|
-
// next) is not judged, by design. The guard sees one call's text and keeps no state: a Bash or PowerShell tool call is a
|
|
10
|
-
// fresh shell, so a variable never survives into the next call - what carries a PID across calls is a file, an environment
|
|
11
|
-
// value or the model's own memory of a literal, none of which the guard can trace to its source. Refusing every non-literal
|
|
12
|
-
// target would refuse `Stop-Process -Id (Get-Content run.pid)` and `kill $(cat run.pid)`, the PID file the remedy itself
|
|
13
|
-
// tells an agent to keep; a query stage is the one evidence of a foreign target, and it is judged only where it is visible,
|
|
14
|
-
// in the same call. Tracking provenance across calls would need a ledger of the PIDs an agent started, which no hook can
|
|
15
|
-
// fill soundly; the by-name rule above and the same-call query rule are the sound part.
|
|
16
|
-
import { assignedCommand } from './assigned-command.mjs';
|
|
17
|
-
const TASKKILL_SELECTOR = /^(?:\/\/?|-)(?:im|fi)$/i;
|
|
18
|
-
const NAME_PARAMETER = /^-(?:n|na|nam|name|processname)(?::.*)?$/i;
|
|
19
|
-
const POWERSHELL_STOP = new Set(['stop-process', 'spps']);
|
|
20
|
-
const REMEDY = 'end only a PID you started yourself, the one you recorded when you started it (taskkill /PID <pid>, kill <pid>, Stop-Process -Id <pid>), never one found by a query; a process you did not start is reported, never killed';
|
|
21
|
-
|
|
22
|
-
/** The refusal for one command that kills by name or pattern, or null. */
|
|
23
|
-
export const nameKillVerdict = (program, args) => {
|
|
24
|
-
let how = null;
|
|
25
|
-
if (program === 'taskkill' && args.some((a) => TASKKILL_SELECTOR.test(a))) how = 'taskkill by image name or filter (/IM, /FI)';
|
|
26
|
-
else if (program === 'pkill' || program === 'killall') how = `${program}, which selects processes by name or pattern`;
|
|
27
|
-
// PowerShell's kill alias takes the full -Name/-ProcessName; a POSIX `kill -n <signal>` is not a name.
|
|
28
|
-
else if ((POWERSHELL_STOP.has(program) && args.some((a) => NAME_PARAMETER.test(a))) || (program === 'kill' && args.some((a) => /^-(?:name|processname)(?::.*)?$/i.test(a)))) how = 'Stop-Process -Name';
|
|
29
|
-
else if (program === 'wmic' && args.some((a) => /^process$/i.test(a)) && args.some((a) => /\bname\s*=|^name$/i.test(a))) how = 'wmic process selected by name';
|
|
30
|
-
// wmic process where <filter> delete | call terminate, unless the filter is one literal ProcessId.
|
|
31
|
-
else if (program === 'wmic' && args.some((a) => /^process$/i.test(a)) && args.some((a) => /\b(?:delete|terminate)\b/i.test(a)) && !args.some((a) => /(?<![a-z])(?:process)?id\s*=\s*\d+\s*$/i.test(a))) how = 'wmic process ... delete|terminate selected by a filter';
|
|
32
|
-
if (!how) return null;
|
|
33
|
-
return { code: 'PROCESS_KILL_BY_NAME', command: [program, ...args].join(' ').slice(0, 200),
|
|
34
|
-
reason: `${how} ends every process it matches on the machine, including Orca and other agents' workers (a lane's taskkill of node.exe restarted Orca, 2026-10-01)`,
|
|
35
|
-
remedy: REMEDY };
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const QUERY_PROGRAMS = new Set(['get-process', 'gps', 'ps', 'pgrep', 'pidof', 'tasklist']);
|
|
39
|
-
const CIM_PROGRAMS = new Set(['get-ciminstance', 'gcim', 'get-wmiobject', 'gwmi']);
|
|
40
|
-
const isQuery = (c) => QUERY_PROGRAMS.has(c.program) || (CIM_PROGRAMS.has(c.program) && c.args.some((a) => /win32_process/i.test(a))) || (c.program === 'wmic' && c.args.some((a) => /^process$/i.test(a)));
|
|
41
|
-
const LITERAL = /^\d+(?:,\d+)*$/;
|
|
42
|
-
const OPTION_VALUE = /^-(?:ea|erroraction|wa|warningaction|ev|errorvariable|ov|outvariable|s|n|signal)$/i;
|
|
43
|
-
const TARGET_OPTION = /^-(?:id|inputobject)$/i;
|
|
44
|
-
|
|
45
|
-
// The targets one kill command names ([] when it names none: its targets come down a pipe or an xargs).
|
|
46
|
-
function targetsOf(args) {
|
|
47
|
-
const out = [];
|
|
48
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
49
|
-
const a = args[i];
|
|
50
|
-
const inline = /^-(?:id|inputobject):(.*)$/i.exec(a);
|
|
51
|
-
if (inline) out.push(inline[1]);
|
|
52
|
-
else if (TARGET_OPTION.test(a)) { if (i + 1 < args.length) { out.push(args[i + 1]); } i += 1; }
|
|
53
|
-
else if (OPTION_VALUE.test(a)) i += 1;
|
|
54
|
-
else if (!a.startsWith('-')) out.push(a);
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 'kill' or 'Stop-Process' when its named targets are not all literal PIDs, else null.
|
|
60
|
-
const killTargetHow = (program, targets) => {
|
|
61
|
-
if (targets.length && targets.every((t) => LITERAL.test(t))) return null;
|
|
62
|
-
return program === 'kill' ? 'kill' : 'Stop-Process';
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// A kill sink whose target is not one literal PID: how it is spelled, or null.
|
|
66
|
-
function sinkOf(c) {
|
|
67
|
-
const { program, args } = c;
|
|
68
|
-
if (POWERSHELL_STOP.has(program) || program === 'kill') {
|
|
69
|
-
if (args.some((a) => /^-l$/.test(a))) return null;
|
|
70
|
-
return killTargetHow(program, targetsOf(args));
|
|
71
|
-
}
|
|
72
|
-
if (program === 'taskkill') {
|
|
73
|
-
const at = args.findIndex((a) => /^(?:\/\/?|-)pid$/i.test(a));
|
|
74
|
-
return at >= 0 && !LITERAL.test(args[at + 1] ?? '') ? 'taskkill /PID' : null;
|
|
75
|
-
}
|
|
76
|
-
if (program === 'invoke-cimmethod' && args.some((a) => /^terminate$/i.test(a))) return 'Invoke-CimMethod Terminate';
|
|
77
|
-
if (/\.(?:terminate|kill)$/.test(program)) return `.${program.split('.').pop()}()`;
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// The variables a kill names in the raw text, once per kill: `Stop-Process -Id $p.Id`, `$p | Stop-Process`, `$p.Kill()`,
|
|
82
|
-
// `kill $pid`, `kill $!`. A kill target that is no plain variable (a $(...), a pipe from a query) names none.
|
|
83
|
-
const ROOTS = [
|
|
84
|
-
new RegExp([
|
|
85
|
-
String.raw`\b(?:stop-process|spps|kill|taskkill)\b`,
|
|
86
|
-
String.raw`(?:\s+(?:-(?:id|inputobject)(?::|(?=\s))|(?:\/\/?|-)pid(?=\s)|-\w+(?=\s+\S*\$)))*`,
|
|
87
|
-
String.raw`\s*"?\$(\w+|!)`,
|
|
88
|
-
].join(''), 'gi'),
|
|
89
|
-
/\$(\w+)\s*\|\s*(?:stop-process|spps)\b/gi,
|
|
90
|
-
/\$(\w+)\.(?:terminate|kill)\s*\(/gi,
|
|
91
|
-
];
|
|
92
|
-
const OWN_START = (root) => new RegExp(root === '!' ? '$^' : String.raw`(?:\$${root}\s*=\s*\(?\s*(?:start-process|start-job|\[[\w.]+\]::start)\b|(?<![\w$])${root}=\$!)`, 'i');
|
|
93
|
-
|
|
94
|
-
/** The refusal for a kill whose targets come from a process query, or null. `commands` are the parsed commands, `text` the raw call. */
|
|
95
|
-
export function queryKillVerdict(parsed, text) {
|
|
96
|
-
const commands = parsed.map(assignedCommand);
|
|
97
|
-
if (!commands.some(isQuery)) return null;
|
|
98
|
-
const sinks = commands.map((c) => [c, sinkOf(c)]).filter(([, how]) => how);
|
|
99
|
-
if (!sinks.length) return null;
|
|
100
|
-
const roots = ROOTS.flatMap((re) => [...String(text).matchAll(re)].map((m) => m[1]));
|
|
101
|
-
if (roots.length >= sinks.length && roots.every((r) => r === '!' || OWN_START(r).test(text))) return null;
|
|
102
|
-
const [c, how] = sinks[0];
|
|
103
|
-
return { code: 'PROCESS_KILL_BY_NAME', command: [c.program, ...c.args].join(' ').slice(0, 200),
|
|
104
|
-
reason: `${how} on targets found by a process query (Get-CimInstance Win32_Process, Get-Process, ps, pgrep) ends other lanes' and agents' processes selected by command line, name, path or parent - not a process you started`,
|
|
105
|
-
remedy: REMEDY };
|
|
106
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// protected-zone.mjs - the reader of modules/kernel/protected-zone.yaml, the ONE declaration of the protected zone
|
|
2
|
-
// (rule R224 RIGHTS_PROTECTED_ZONE), and the question the command guard asks of it: is this path inside a runtime checkout,
|
|
3
|
-
// and does it fall in a zone or on a protected catalog entry?
|
|
4
|
-
//
|
|
5
|
-
// A runtime checkout is found by rights.mjs runtimeRootOf (any directory tree holding knowledge/hfs/runtime-slots.yaml: the live
|
|
6
|
-
// .claude, a lane worktree of it, a staging checkout), so every worktree of the runtime is judged by the same declaration
|
|
7
|
-
// without asking git. Pure fs and one YAML read, loaded only when a role that can be
|
|
8
|
-
// refused writes a file (the guard's common path never loads it).
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { parseYaml } from '../../engine/yaml.mjs';
|
|
13
|
-
import { globExpression } from '../lib/glob.mjs';
|
|
14
|
-
import { pathKey, posixPath } from '../lib/path-key.mjs';
|
|
15
|
-
import { runtimeRootOf } from './rights.mjs';
|
|
16
|
-
|
|
17
|
-
const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
18
|
-
const PROTECTED_ZONE_FILE = 'modules/kernel/protected-zone.yaml';
|
|
19
|
-
|
|
20
|
-
const cache = new Map();
|
|
21
|
-
|
|
22
|
-
/** {zones: [{id, why, matchers}], catalog: [{file, ids, codes}]} parsed from the declaration of `root`, or an empty zone when unreadable. */
|
|
23
|
-
function loadProtectedZone({ root = SKILL_ROOT, read = (file) => fs.readFileSync(file, 'utf8') } = {}) {
|
|
24
|
-
const key = pathKey(root);
|
|
25
|
-
if (cache.has(key)) return cache.get(key);
|
|
26
|
-
let parsed = null;
|
|
27
|
-
try { parsed = parseYaml(read(path.join(root, PROTECTED_ZONE_FILE))); } catch { parsed = null; }
|
|
28
|
-
const zone = {
|
|
29
|
-
zones: (parsed?.zones ?? []).map((z) => ({ id: String(z.id), why: String(z.why ?? ''), matchers: (z.paths ?? []).map((p) => globExpression(String(p))) })),
|
|
30
|
-
catalog: (parsed?.catalogEntries ?? []).map((c) => ({ file: posixPath(String(c.file)), ids: (c.ids ?? []).map(String), codes: (c.codes ?? []).map(String) })),
|
|
31
|
-
};
|
|
32
|
-
cache.set(key, zone);
|
|
33
|
-
return zone;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Where `file` stands against the zone: {runtimeRoot, rel, zone: {id, why} | null, catalog: entry | null}. runtimeRoot is
|
|
38
|
-
* null when the path is outside every runtime checkout (then nothing here applies).
|
|
39
|
-
*/
|
|
40
|
-
export function zoneOfPath(file, { declaration = loadProtectedZone(), exists = fs.existsSync } = {}) {
|
|
41
|
-
const runtimeRoot = runtimeRootOf(file, { exists });
|
|
42
|
-
if (!runtimeRoot) return { runtimeRoot: null, rel: null, zone: null, catalog: null };
|
|
43
|
-
const rel = posixPath(path.relative(runtimeRoot, path.resolve(file)));
|
|
44
|
-
const zone = declaration.zones.find((z) => z.matchers.some((m) => m.test(rel))) ?? null;
|
|
45
|
-
const catalog = declaration.catalog.find((c) => c.file === rel) ?? null;
|
|
46
|
-
return { runtimeRoot, rel, zone: zone ? { id: zone.id, why: zone.why } : null, catalog };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The ids or codes of a catalog entry that `text` names (a rule id as a whole token, a code as a substring), or []. */
|
|
50
|
-
export function catalogNames(entry, text) {
|
|
51
|
-
const body = String(text ?? '');
|
|
52
|
-
const hits = [];
|
|
53
|
-
for (const id of entry.ids) if (new RegExp(`(?:^|[^A-Za-z0-9])${id}(?![0-9])`).test(body)) hits.push(id);
|
|
54
|
-
for (const code of entry.codes) if (body.includes(code)) hits.push(code);
|
|
55
|
-
return hits;
|
|
56
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// refusals.mjs — how an op guard refuses: the lines the agent reads and the <guards root>/refusals.jsonl record.
|
|
2
|
-
// Shared by the agent command guard (command-guard.mjs, a PreToolUse hook) and the history hook's commit check
|
|
3
|
-
// (verify-commit.mjs).
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { guardsRoot } from './guards-root.mjs';
|
|
8
|
-
|
|
9
|
-
const skillRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
-
|
|
11
|
-
/** The refusal as the agent reads it: what was refused and why, the remedy, and the shared-checkout rule. */
|
|
12
|
-
export function refusalLines(tool, verdict) {
|
|
13
|
-
return [
|
|
14
|
-
`starci guard: refused \`${tool} ${verdict.command ?? ''}\` [${verdict.code}] — ${verdict.reason}.`,
|
|
15
|
-
...(verdict.remedy ? [`starci guard: instead: ${verdict.remedy}.`] : []),
|
|
16
|
-
'starci guard: this checkout is shared with other workflows (modules/ops/_common.yaml "Evidence, completion and commits"). Report a real need in your report; never work around this guard.',
|
|
17
|
-
];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Append one refusal to <guards root>/refusals.jsonl; the refusal stands without its log line. */
|
|
21
|
-
export function logRefusal(entry, { root = skillRoot } = {}) {
|
|
22
|
-
try {
|
|
23
|
-
const dir = guardsRoot(root);
|
|
24
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
-
fs.appendFileSync(path.join(dir, 'refusals.jsonl'), `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`);
|
|
26
|
-
} catch { /* the refusal stands without its log line */ }
|
|
27
|
-
}
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
// rights.mjs - the ROLE RIGHTS of the PreToolUse command guard (rules R223 RIGHTS_ROLE_DENIED, R224 RIGHTS_PROTECTED_ZONE).
|
|
2
|
-
//
|
|
3
|
-
// Who may do what is decided by the caller's role, which the guard already resolves from the Orca terminal the agent
|
|
4
|
-
// runs in (scripts/guards/command-guard.mjs boundGuard, hook-install.mjs bindSeatGuard) and never from anything the agent
|
|
5
|
-
// says about itself:
|
|
6
|
-
// op an op worker of a workflow on an app (job guard, role op): path 3 of the source process
|
|
7
|
-
// supervisor the Supervisor seat (seat guard) and its [Worker] jobs (job guard of workflow 'supervisor'): path 2, the
|
|
8
|
-
// self-upgrade of .claude
|
|
9
|
-
// lead the Kernel (job guard, role kernel)
|
|
10
|
-
// coordinator a session launched with STARCI_ROLE=coordinator (the land seat)
|
|
11
|
-
// release STARCI_ROLE=release AND the host lock held live by role release (scripts/machine/host-lock.mjs): the release cut
|
|
12
|
-
// null no guard, no seat, no role: the owner's own session, which the rights never restrict
|
|
13
|
-
//
|
|
14
|
-
// Command admission is data in modules/kernel/command-policy.yaml and is evaluated by command-policy.mjs. This module keeps
|
|
15
|
-
// only role resolution and the small parsers that both the PreToolUse guard and PATH shim share. It imports nothing heavy:
|
|
16
|
-
// the YAML of either policy loads only after a caller is known to have a bound role.
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import { guardsRoot } from './guards-root.mjs';
|
|
20
|
-
import { pathKey, sameOrUnder } from '../lib/path-key.mjs';
|
|
21
|
-
import { RUNTIME_CHANGE_CODE, runtimeWriteRefusal } from '../machine/runtime-change.mjs';
|
|
22
|
-
import { opWriteScopeRefusal } from './op-write-scope.mjs';
|
|
23
|
-
|
|
24
|
-
const ENV_ROLES = new Set(['op', 'supervisor', 'lead', 'coordinator', 'release']);
|
|
25
|
-
const safeName = (s) => String(s).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
26
|
-
const RUNTIME_MARKER = path.join('knowledge', 'hfs', 'runtime-slots.yaml');
|
|
27
|
-
const WALK_LIMIT = 24;
|
|
28
|
-
|
|
29
|
-
/** <guards root>/seats/<handle>.json (the seat guard bound to an Orca terminal), or null. */
|
|
30
|
-
export function boundSeat(handle, { root, env = process.env } = {}) {
|
|
31
|
-
if (!handle) return null;
|
|
32
|
-
try { return JSON.parse(fs.readFileSync(path.join(guardsRoot(root, env), 'seats', `${safeName(handle)}.json`), 'utf8')); }
|
|
33
|
-
catch { return null; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** <guards root>/terminals/<handle>.json (the terminal-bound job guard), or null. */
|
|
37
|
-
export function boundGuard(handle, { root, env = process.env } = {}) {
|
|
38
|
-
if (!handle) return null;
|
|
39
|
-
try { return JSON.parse(fs.readFileSync(path.join(guardsRoot(root, env), 'terminals', `${safeName(handle)}.json`), 'utf8')); }
|
|
40
|
-
catch { return null; }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Actual terminal/seat binding: missing is absent; unreadable, linked or contradictory is unknown. */
|
|
44
|
-
export function boundIdentityOf(handle, { root, env = process.env } = {}) {
|
|
45
|
-
const read = (family) => {
|
|
46
|
-
const file = path.join(guardsRoot(root, env), family, `${safeName(handle)}.json`);
|
|
47
|
-
let stat;
|
|
48
|
-
try { stat = fs.lstatSync(file); } catch (error) { if (error.code === 'ENOENT') { return null; } throw error; }
|
|
49
|
-
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('non-regular caller binding');
|
|
50
|
-
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
51
|
-
if (!value || typeof value !== 'object' || Array.isArray(value) || value.terminal !== handle) throw new Error('contradictory caller binding');
|
|
52
|
-
return value;
|
|
53
|
-
};
|
|
54
|
-
if (!handle) return { guard: null, seat: null };
|
|
55
|
-
return { guard: read('terminals'), seat: read('seats') };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The caller's rights role, or null (the owner's own session). `lockOwner` is the host lock's owner object (or null): the
|
|
60
|
-
* release role needs it, so a session cannot claim release by an environment variable alone.
|
|
61
|
-
*/
|
|
62
|
-
export function rightsRoleOf({ guard = null, seat = null, env = process.env, lockOwner = null } = {}) {
|
|
63
|
-
const claimed = String(env?.STARCI_ROLE ?? '').toLowerCase();
|
|
64
|
-
if (guard?.role === 'kernel') return 'lead';
|
|
65
|
-
if (guard?.role === 'critic') return 'critic';
|
|
66
|
-
if (guard?.role === 'op') return guard.workflowId === 'supervisor' ? 'supervisor' : 'op';
|
|
67
|
-
if (seat?.role === 'supervisor') return 'supervisor';
|
|
68
|
-
// A claim never outranks a bound guard or seat; the release claim holds only while the host lock is held live by role release.
|
|
69
|
-
if (claimed === 'release') return lockOwner && !lockOwner.stale && lockOwner.role === 'release' ? 'release' : null;
|
|
70
|
-
if (ENV_ROLES.has(claimed)) return claimed;
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* ------------------------------------------------------------------------------------------ command-policy parsers */
|
|
75
|
-
|
|
76
|
-
const GIT_GLOBAL_WITH_VALUE = new Set(['--git-dir', '--work-tree', '--namespace', '--super-prefix', '--exec-path', '--attr-source']);
|
|
77
|
-
|
|
78
|
-
/** {sub, rest} of `git [global options] <sub> <args...>`. */
|
|
79
|
-
export function gitSubOf(args) {
|
|
80
|
-
let i = 0;
|
|
81
|
-
while (i < args.length) {
|
|
82
|
-
const a = String(args[i]);
|
|
83
|
-
if ((a === '-C' || a === '-c' || a === '--config-env' || GIT_GLOBAL_WITH_VALUE.has(a)) && i + 1 < args.length) { i += 2; continue; }
|
|
84
|
-
if (a.startsWith('-')) { i += 1; continue; }
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
return { sub: args[i] == null ? null : String(args[i]), rest: args.slice(i + 1).map(String) };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const PUSH_VALUE_OPTIONS = new Set(['-o', '--push-option', '--repo', '--receive-pack', '--exec']);
|
|
91
|
-
const PUSH_ALL = /^--(?:all|mirror|tags|follow-tags|prune)$/;
|
|
92
|
-
|
|
93
|
-
/** The destination refs of one `git push` call: {remote, refs[], broad}. broad = the call pushes more than the refspecs name. */
|
|
94
|
-
export function pushTargets(rest) {
|
|
95
|
-
const words = [];
|
|
96
|
-
let broad = false;
|
|
97
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
98
|
-
const a = rest[i];
|
|
99
|
-
if (PUSH_VALUE_OPTIONS.has(a)) { i += 1; continue; }
|
|
100
|
-
if (a.startsWith('-')) { if (PUSH_ALL.test(a)) { broad = true; } continue; }
|
|
101
|
-
words.push(a);
|
|
102
|
-
}
|
|
103
|
-
const refs = words.slice(1).map((spec) => {
|
|
104
|
-
const s = spec.replace(/^\+/, '');
|
|
105
|
-
const dst = s.includes(':') ? s.slice(s.indexOf(':') + 1) : s;
|
|
106
|
-
return dst.replace(/^refs\/heads\//, 'refs/heads/');
|
|
107
|
-
});
|
|
108
|
-
return { remote: words[0] ?? null, refs, broad: broad || words.length < 2 };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const optionName = (value) => String(value).split('=', 1)[0];
|
|
112
|
-
const optionWords = (rest, takesValue = new Set()) => {
|
|
113
|
-
const options = [], words = [];
|
|
114
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
115
|
-
const value = String(rest[i]);
|
|
116
|
-
if (value === '--') { words.push(...rest.slice(i + 1).map(String)); break; }
|
|
117
|
-
if (!value.startsWith('-')) { words.push(value); continue; }
|
|
118
|
-
options.push(value);
|
|
119
|
-
if (!value.includes('=') && takesValue.has(optionName(value)) && rest[i + 1] != null) words.push(String(rest[++i]));
|
|
120
|
-
}
|
|
121
|
-
return { options, words };
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const BRANCH_READ = new Set(['--list', '-l', '-a', '--all', '-r', '--remotes', '-v', '--verbose', '--show-current', '--contains', '--merged', '--points-at']);
|
|
125
|
-
const BRANCH_VALUES = new Set(['--contains', '--merged', '--points-at']);
|
|
126
|
-
const TAG_READ = new Set(['-l', '--list', '-v', '--verify', '--points-at', '--contains', '--merged', '-n']);
|
|
127
|
-
const TAG_VALUES = new Set(['--points-at', '--contains', '--merged']);
|
|
128
|
-
const CONFIG_READ = new Set(['--get', '--get-all', '--get-regexp', '--list', '-l', '--show-origin']);
|
|
129
|
-
|
|
130
|
-
const branchListRead = (list) => {
|
|
131
|
-
const { options, words } = optionWords(list, BRANCH_VALUES);
|
|
132
|
-
if (options.some((value) => !BRANCH_READ.has(optionName(value)))) return false;
|
|
133
|
-
const valueCount = options.filter((value) => BRANCH_VALUES.has(optionName(value)) && !value.includes('=')).length;
|
|
134
|
-
return options.some((value) => ['--list', '-l'].includes(optionName(value))) || words.length <= valueCount;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const tagListRead = (list) => {
|
|
138
|
-
if (!list.length) return true;
|
|
139
|
-
const { options, words } = optionWords(list, TAG_VALUES);
|
|
140
|
-
const normalized = options.map((value) => /^-n\d*$/.test(value) ? '-n' : optionName(value));
|
|
141
|
-
if (normalized.some((value) => !TAG_READ.has(value))) return false;
|
|
142
|
-
const valueCount = normalized.filter((value, index) => TAG_VALUES.has(value) && !options[index].includes('=')).length;
|
|
143
|
-
if (normalized.includes('-l') || normalized.includes('--list')) return true;
|
|
144
|
-
return normalized.some((value) => TAG_READ.has(value)) && (normalized.includes('-v') || normalized.includes('--verify') ? words.length <= valueCount + 1 : words.length <= valueCount);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/** Whether one of git's mixed read/write subcommands is in an explicitly read-only form. */
|
|
148
|
-
export function gitListFormRead(sub, rest) {
|
|
149
|
-
const list = rest.map(String);
|
|
150
|
-
if (sub === 'branch') return branchListRead(list);
|
|
151
|
-
if (sub === 'tag') return tagListRead(list);
|
|
152
|
-
const words = list.filter((value) => !value.startsWith('-'));
|
|
153
|
-
if (sub === 'stash') return ['list', 'show'].includes(words[0]) && !words.slice(1).includes('push');
|
|
154
|
-
if (sub === 'worktree') return words[0] === 'list';
|
|
155
|
-
if (sub === 'remote') {
|
|
156
|
-
const options = list.filter((value) => value.startsWith('-'));
|
|
157
|
-
if (options.some((value) => !['-v', '--verbose', '--all', '--push'].includes(optionName(value)))) return false;
|
|
158
|
-
return !words.length || (words[0] === 'show' && words.length <= 2) || (words[0] === 'get-url' && words.length <= 2);
|
|
159
|
-
}
|
|
160
|
-
if (sub === 'config') return list.some((value) => CONFIG_READ.has(optionName(value)))
|
|
161
|
-
&& !list.some((value) => value.startsWith('-') && !CONFIG_READ.has(optionName(value)));
|
|
162
|
-
if (sub === 'reflog') return !words.length || (words[0] === 'show' && words.length <= 2);
|
|
163
|
-
if (sub === 'notes') return ['list', 'show'].includes(words[0]);
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const NODE_VALUE_FLAGS = new Set(['--import', '--require', '-r', '--loader', '--experimental-loader', '--conditions', '-C', '--test-reporter', '--test-reporter-destination', '--test-concurrency', '--test-name-pattern', '--test-skip-pattern', '--test-timeout', '--eval', '-e', '--print', '-p']);
|
|
168
|
-
const WHOLE_TREE = [/(?:^|\/)tests\/\*\*/, /^\*\*\/\*\.(?:spec|test)\.m?js$/, /^(?:\.\/)?tests\/?$/];
|
|
169
|
-
|
|
170
|
-
/** True when a `node --test` call names no file or names the whole tests tree (a glob over tests/**, or the tests directory itself). */
|
|
171
|
-
export function nodeWholeSuite(args) {
|
|
172
|
-
const list = args.map(String);
|
|
173
|
-
if (!list.includes('--test')) return false;
|
|
174
|
-
const targets = [];
|
|
175
|
-
for (let i = 0; i < list.length; i += 1) {
|
|
176
|
-
if (NODE_VALUE_FLAGS.has(list[i])) { i += 1; continue; }
|
|
177
|
-
if (!list[i].startsWith('-')) targets.push(list[i].replaceAll('\\', '/'));
|
|
178
|
-
}
|
|
179
|
-
return !targets.length || targets.some((x) => WHOLE_TREE.some((re) => re.test(x)));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/* ----------------------------------------------------------------------------------------------------- file writes */
|
|
183
|
-
|
|
184
|
-
/** One refusal of the command guard: {code, command, reason, remedy}, plus `use` (the starci verb) when the table names one. */
|
|
185
|
-
export const refusal = (code, command, reason, remedy, use = null) => ({ code, command: String(command).slice(0, 200), reason, remedy, ...(use ? { use } : {}) });
|
|
186
|
-
|
|
187
|
-
/** The runtime checkout root a path lies in (the nearest ancestor holding the runtime marker), or null. */
|
|
188
|
-
export function runtimeRootOf(file, { exists = fs.existsSync } = {}) {
|
|
189
|
-
let dir = path.dirname(path.resolve(file));
|
|
190
|
-
for (let i = 0; i < WALK_LIMIT; i += 1) {
|
|
191
|
-
if (exists(path.join(dir, RUNTIME_MARKER))) return dir;
|
|
192
|
-
const up = path.dirname(dir);
|
|
193
|
-
if (up === dir) return null;
|
|
194
|
-
dir = up;
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* The refusal of one file write (an Edit/Write tool call or a shell write target), or null. `edit` is the text the call
|
|
202
|
-
* adds or replaces ({old, new}), read for catalog entries; `declaration` and `zoneOfPath` are injected by the guard (the
|
|
203
|
-
* YAML loads only here, only for a role that can be refused).
|
|
204
|
-
*/
|
|
205
|
-
export function fileWriteVerdict({ role, filePath, tool = 'Edit', edit = null, guard = null, zone = null, shell = false }) {
|
|
206
|
-
if (role !== 'supervisor' && role !== 'op') return null;
|
|
207
|
-
const file = path.resolve(filePath);
|
|
208
|
-
if (role === 'op') {
|
|
209
|
-
const runtimeRoot = zone?.runtimeRoot ?? runtimeRootOf(file);
|
|
210
|
-
const own = [guard?.workflowWorktree, ...(guard?.owned ?? [])].filter(Boolean);
|
|
211
|
-
if (!runtimeRoot || own.some((dir) => sameOrUnder(pathKey(file), pathKey(dir)))) return opScopeVerdict(file, guard);
|
|
212
|
-
return refusal('RIGHTS_OP_RUNTIME_WRITE', file, 'an op works on its app in the workflow worktree and never writes inside the .claude runtime checkout: a defect of the harness is not the op\'s to fix',
|
|
213
|
-
'record a lesson or an upgrade request in your report (kernel report: lessons / upgrade request); the owner path or the supervisor self-upgrade picks it up');
|
|
214
|
-
}
|
|
215
|
-
if (!zone?.runtimeRoot) return null;
|
|
216
|
-
if (zone.zone) {
|
|
217
|
-
return refusal('RIGHTS_PROTECTED_ZONE', zone.rel, `${zone.rel} is in the protected zone (${zone.zone.id}: ${zone.zone.why}): a supervisor self-upgrade does not edit it`,
|
|
218
|
-
'file an owner proposal (what to change in the protected zone and why); only the owner path edits the guard, the release cut, the git hooks, the CI triggers, the test policy, the host lock and the permission settings');
|
|
219
|
-
}
|
|
220
|
-
if (zone.catalog) {
|
|
221
|
-
const verdict = catalogWriteVerdict({ zone, shell, tool, edit });
|
|
222
|
-
if (verdict) return verdict;
|
|
223
|
-
}
|
|
224
|
-
const change = runtimeWriteRefusal();
|
|
225
|
-
return refusal(RUNTIME_CHANGE_CODE, zone.rel, change.reason, change.remedy);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** The refusal of an Op write outside its owned paths (op-write-scope.mjs), or null. */
|
|
229
|
-
function opScopeVerdict(file, guard) {
|
|
230
|
-
const outside = opWriteScopeRefusal({ file, guard });
|
|
231
|
-
return outside ? refusal(outside.code, file, outside.reason, outside.remedy) : null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** The protected-zone refusal for a catalog write, or null when no protected entry is named. */
|
|
235
|
-
function catalogWriteVerdict({ zone, shell, tool, edit }) {
|
|
236
|
-
const named = shell || tool === 'Write' ? ['whole-file write'] : (zone.catalogNames?.(zone.catalog, `${edit?.old ?? ''}\n${edit?.new ?? ''}`) ?? []);
|
|
237
|
-
if (!named.length) return null;
|
|
238
|
-
const how = shell || tool === 'Write' ? 'rewrites the whole file' : `names ${named.join(', ')}`;
|
|
239
|
-
return refusal('RIGHTS_PROTECTED_ZONE', zone.rel, `${zone.rel} holds catalog entries that enforce the protected zone (${zone.catalog.ids.concat(zone.catalog.codes).slice(0, 6).join(', ')}...): this write ${how}`,
|
|
240
|
-
'edit other entries with the Edit tool (an edit that names no protected rule id or code passes); a change to a protected entry is an owner proposal');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const WRITE_LAST = new Set(['cp', 'install', 'copy', 'copy-item', 'cpi']);
|
|
244
|
-
const WRITE_ALL = new Set(['mv', 'move', 'move-item', 'mi', 'ren', 'rename', 'rename-item', 'rni', 'rm', 'rmdir', 'rd', 'del', 'erase', 'remove-item', 'ri', 'tee', 'touch', 'truncate', 'set-content', 'sc', 'add-content', 'ac', 'out-file', 'clear-content', 'clc', 'new-item', 'ni', 'mkdir']);
|
|
245
|
-
const IN_PLACE = new Set(['sed', 'perl', 'ruby']);
|
|
246
|
-
|
|
247
|
-
/** The absolute paths one parsed command writes by a known writer program (cp, mv, rm, tee, sed -i, Set-Content, ...). */
|
|
248
|
-
export function writeTargetsOf(command) {
|
|
249
|
-
const { program, args, cwd } = command;
|
|
250
|
-
const operands = args.map(String).filter((a) => !a.startsWith('-') && !/^[A-Za-z][A-Za-z0-9]*:$/.test(a));
|
|
251
|
-
let picks = [];
|
|
252
|
-
if (WRITE_LAST.has(program)) picks = operands.slice(-1);
|
|
253
|
-
else if (WRITE_ALL.has(program)) picks = operands;
|
|
254
|
-
else if (IN_PLACE.has(program) && args.some((a) => /^-[a-zA-Z]*i/.test(String(a)))) picks = operands.slice(1);
|
|
255
|
-
else if (program === 'dd') picks = args.map(String).filter((a) => a.startsWith('of=')).map((a) => a.slice(3));
|
|
256
|
-
return picks.filter((p) => p && !/[*?]/.test(p)).map((p) => path.resolve(cwd ?? process.cwd(), p));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** The redirection targets (`> file`, `>> file`, `*> file`) of a raw command text, resolved against `cwd`. */
|
|
260
|
-
export function redirectTargetsOf(text, cwd = process.cwd()) {
|
|
261
|
-
const out = [];
|
|
262
|
-
for (const m of String(text ?? '').matchAll(/(?:^|[^<>&0-9])(?:\d|\*)?>{1,2}\s*("[^"\n]+"|'[^'\n]+'|[^\s;&|()<>"']+)/g)) {
|
|
263
|
-
const target = m[1].replace(/^["']|["']$/g, '');
|
|
264
|
-
if (/^(?:&|\/dev\/null$|\$null$|nul$)/i.test(target)) continue;
|
|
265
|
-
out.push(path.resolve(cwd, target));
|
|
266
|
-
}
|
|
267
|
-
return out;
|
|
268
|
-
}
|