create-agent-rig 0.7.1 → 0.9.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/CHANGELOG.md +184 -2
- package/README.md +93 -4
- package/package.json +4 -3
- package/packages/cli/dist/commands/memory.js +123 -0
- package/packages/cli/dist/commands/setup.js +45 -0
- package/packages/cli/dist/index.js +107 -3
- package/packages/cli/dist/lib/subsystems.js +269 -0
- package/packages/cli/dist/lib/version.js +15 -0
- package/packages/cli/dist/policy/benchmark/corpus.js +165 -0
- package/packages/cli/dist/policy/core/adapter.js +18 -0
- package/packages/cli/dist/policy/core/coverage.js +253 -0
- package/packages/cli/dist/policy/core/decision-record.js +287 -0
- package/packages/cli/dist/policy/core/declaration.js +127 -0
- package/packages/cli/dist/policy/core/evidence-matrix.js +94 -0
- package/packages/cli/dist/policy/core/probe.js +442 -0
- package/packages/cli/dist/policy/core/registry.js +115 -0
- package/packages/cli/dist/policy/core/validation.js +275 -0
- package/packages/cli/dist/policy/core/vocabulary.js +123 -0
- package/packages/cli/dist/policy/harness/claude.js +47 -0
- package/packages/cli/dist/policy/harness/codex.js +87 -0
- package/packages/cli/dist/policy/harness/index.js +15 -0
- package/packages/cli/dist/policy/harness/shared-hooks.js +28 -0
- package/packages/cli/dist/policy/index.js +17 -0
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +2 -0
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +2 -0
- package/templates/agent-os/subagent-routing.json +32 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +48 -7
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +2 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +2 -0
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +2 -0
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -0
- package/templates/agent-os/universal/.claude/hooks/guard-subagent-model.mjs +234 -0
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +75 -32
- package/templates/agent-os/universal/.claude/hooks/warn-subagent-routing.mjs +120 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +17 -7
- package/templates/agent-os/universal/.claude/rules/workflow.md +5 -0
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +27 -3
- package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +70 -2
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +12 -4
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +64 -1
- package/templates/agent-os/universal/.claude/settings.json +16 -0
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +48 -7
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +2 -0
- package/templates/agent-os/universal/.codex/config.toml +3 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +31 -5
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +142 -0
- package/templates/agent-os/universal/layers.json +4 -0
- package/templates/hash-history.json +15 -7
- package/templates/release-ledger.json +3 -1
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +3 -4
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +40 -0
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +51 -0
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +9 -8
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { CreateError, createProject } from './commands/create.js';
|
|
4
4
|
import { InitError, initFileContents, initProject, planInit } from './commands/init.js';
|
|
5
|
+
import { execFileRunner, setupSubsystems } from './commands/setup.js';
|
|
5
6
|
import { UpgradeError, applyUpgrade, planUpgrade } from './commands/upgrade.js';
|
|
6
7
|
import { makePalette } from './lib/colors.js';
|
|
7
8
|
import { readManifest } from './lib/manifest.js';
|
|
9
|
+
import { SubsystemsError, refreshSubsystems, subsystemsManifestPath } from './lib/subsystems.js';
|
|
8
10
|
import { promptConfirm, promptTarget } from './lib/prompts.js';
|
|
9
11
|
import { collectGovernance, renderSummary } from './lib/summary.js';
|
|
10
12
|
import { DEFAULT_TARGET, TARGET_NAMES } from './lib/targets.js';
|
|
11
|
-
import { packageVersion } from './lib/version.js';
|
|
13
|
+
import { packageVersion, rigHandshake } from './lib/version.js';
|
|
14
|
+
import { runMemory } from './commands/memory.js';
|
|
12
15
|
const USAGE = `Usage: create-agent-rig <dir> [options]
|
|
13
16
|
|
|
14
17
|
Scaffolds a new project into <dir>: a Claude Code + Codex agent operating system
|
|
@@ -20,7 +23,8 @@ Options
|
|
|
20
23
|
required when not a terminal — default: ${DEFAULT_TARGET})
|
|
21
24
|
--no-git skip git init + the pristine-template baseline commit
|
|
22
25
|
--no-color plain output (NO_COLOR is respected too)
|
|
23
|
-
--version print the version
|
|
26
|
+
--version print the version (--version --json: the contract handshake,
|
|
27
|
+
one JSON object with the name, version and contract version)
|
|
24
28
|
-h, --help this text
|
|
25
29
|
|
|
26
30
|
Also: create-agent-rig init [--dry-run]
|
|
@@ -32,7 +36,68 @@ Also: create-agent-rig init [--dry-run]
|
|
|
32
36
|
|
|
33
37
|
Also: create-agent-rig upgrade [--dry-run] [--yes]
|
|
34
38
|
Bring the rig in the CURRENT repo up to this version. Replaces the files it
|
|
35
|
-
installed and you did not touch; everything else is reported, never merged
|
|
39
|
+
installed and you did not touch; everything else is reported, never merged.
|
|
40
|
+
Re-runs the subsystem manifest derivation when one exists (see setup).
|
|
41
|
+
|
|
42
|
+
Also: create-agent-rig setup --memory-root <checkout> [--memory-ref <sha>] [--dry-run]
|
|
43
|
+
Record the Memory executable in this machine's subsystem manifest
|
|
44
|
+
(~/.config/create-agent-rig/subsystems.json; %APPDATA% on Windows) from the
|
|
45
|
+
one declared root. Performs the --version --json handshake first and refuses
|
|
46
|
+
a foreign contract major with exit 4 before writing anything.
|
|
47
|
+
|
|
48
|
+
Also: create-agent-rig memory <doctor|load> [args…]
|
|
49
|
+
Run a Memory verb through the registered executable: the --version --json
|
|
50
|
+
handshake first (a foreign contract major exits 4 and the verb never runs),
|
|
51
|
+
then the verb and its arguments verbatim, Memory's answer passed through
|
|
52
|
+
unchanged. No manifest answers unsupported/absent (exit 0); an invalid
|
|
53
|
+
invocation exits 2.`;
|
|
54
|
+
async function runSetup(rawArgs) {
|
|
55
|
+
let values;
|
|
56
|
+
try {
|
|
57
|
+
({ values } = parseArgs({
|
|
58
|
+
args: rawArgs,
|
|
59
|
+
options: {
|
|
60
|
+
'memory-root': { type: 'string' },
|
|
61
|
+
'memory-ref': { type: 'string' },
|
|
62
|
+
'dry-run': { type: 'boolean' },
|
|
63
|
+
'no-color': { type: 'boolean' },
|
|
64
|
+
},
|
|
65
|
+
allowPositionals: false,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
process.stderr.write(`${error.message}\n\n${USAGE}\n`);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
const memoryRoot = values['memory-root'];
|
|
73
|
+
if (memoryRoot === undefined) {
|
|
74
|
+
process.stderr.write(`setup needs --memory-root <checkout>\n\n${USAGE}\n`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const result = await setupSubsystems({
|
|
79
|
+
memoryRoot,
|
|
80
|
+
memoryRef: values['memory-ref'] ?? null,
|
|
81
|
+
dryRun: values['dry-run'] === true,
|
|
82
|
+
});
|
|
83
|
+
if (result.outcome === 'refused') {
|
|
84
|
+
process.stderr.write(`Memory handshake: ${JSON.stringify(result.handshake)}\n`);
|
|
85
|
+
return result.exitCode;
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`Memory handshake: ${JSON.stringify(result.handshake)}\n` +
|
|
88
|
+
(result.outcome === 'dry-run'
|
|
89
|
+
? `Dry run — nothing written (would write ${result.file}).\n`
|
|
90
|
+
: `Wrote ${result.file}\n`));
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof SubsystemsError) {
|
|
95
|
+
process.stderr.write(`setup: ${error.message} (${error.code})\n`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
36
101
|
async function runInit(rawArgs) {
|
|
37
102
|
let values;
|
|
38
103
|
try {
|
|
@@ -219,15 +284,47 @@ async function runUpgrade(rawArgs) {
|
|
|
219
284
|
}
|
|
220
285
|
const result = await applyUpgrade(cwd, plan);
|
|
221
286
|
process.stdout.write(`\nWrote ${result.written.length} files.\n`);
|
|
287
|
+
// The subsystem manifest is machine-scoped and written by `setup`; an
|
|
288
|
+
// upgrade re-runs the same derivation so `installedVersion` follows the
|
|
289
|
+
// executable the root now holds. It is never part of the rig manifest or of
|
|
290
|
+
// `result.written` — that file list is the repository's, this one is the
|
|
291
|
+
// user's. Absent stays absent: creating it is `setup`'s act.
|
|
292
|
+
try {
|
|
293
|
+
const refreshed = await refreshSubsystems({
|
|
294
|
+
file: subsystemsManifestPath(process.env, process.platform),
|
|
295
|
+
run: execFileRunner,
|
|
296
|
+
nodeExecutable: process.execPath,
|
|
297
|
+
platform: process.platform,
|
|
298
|
+
});
|
|
299
|
+
if (refreshed !== 'absent')
|
|
300
|
+
process.stdout.write(`Subsystem manifest: ${typeof refreshed === 'string' ? refreshed : JSON.stringify(refreshed)}\n`);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (!(error instanceof SubsystemsError))
|
|
304
|
+
throw error;
|
|
305
|
+
process.stdout.write(`Subsystem manifest: not refreshed — ${error.message} (${error.code})\n`);
|
|
306
|
+
}
|
|
222
307
|
return 0;
|
|
223
308
|
}
|
|
224
309
|
async function main() {
|
|
225
310
|
if (process.argv[2] === 'init') {
|
|
226
311
|
return runInit(process.argv.slice(3));
|
|
227
312
|
}
|
|
313
|
+
if (process.argv[2] === 'setup') {
|
|
314
|
+
return runSetup(process.argv.slice(3));
|
|
315
|
+
}
|
|
228
316
|
if (process.argv[2] === 'upgrade') {
|
|
229
317
|
return runUpgrade(process.argv.slice(3));
|
|
230
318
|
}
|
|
319
|
+
if (process.argv[2] === 'memory') {
|
|
320
|
+
// The consumer path of the RP-19 handshake: manifest → `--version --json`
|
|
321
|
+
// → exit 4 on a foreign major → doctor/load passed through verbatim.
|
|
322
|
+
const [verb = '', ...args] = process.argv.slice(3);
|
|
323
|
+
const result = await runMemory({ verb, args });
|
|
324
|
+
process.stdout.write(result.stdout);
|
|
325
|
+
process.stderr.write(result.stderr);
|
|
326
|
+
return result.exitCode;
|
|
327
|
+
}
|
|
231
328
|
let positionals;
|
|
232
329
|
let values;
|
|
233
330
|
try {
|
|
@@ -237,6 +334,9 @@ async function main() {
|
|
|
237
334
|
help: { type: 'boolean', short: 'h' },
|
|
238
335
|
target: { type: 'string' },
|
|
239
336
|
version: { type: 'boolean' },
|
|
337
|
+
// `--json` is read on `--version` alone: the handshake object of
|
|
338
|
+
// docs/command-contract.md, one JSON line and nothing else on stdout.
|
|
339
|
+
json: { type: 'boolean' },
|
|
240
340
|
'no-git': { type: 'boolean' },
|
|
241
341
|
'no-color': { type: 'boolean' },
|
|
242
342
|
},
|
|
@@ -248,6 +348,10 @@ async function main() {
|
|
|
248
348
|
return 1;
|
|
249
349
|
}
|
|
250
350
|
if (values.version) {
|
|
351
|
+
if (values.json) {
|
|
352
|
+
process.stdout.write(`${JSON.stringify(await rigHandshake())}\n`);
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
251
355
|
process.stdout.write(`${await packageVersion()}\n`);
|
|
252
356
|
return 0;
|
|
253
357
|
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// The machine-scoped subsystem manifest (ADR-RP-002 R6, RP-147): installation
|
|
2
|
+
// metadata, not a service registry. One writer (`setup`; `upgrade` re-runs the
|
|
3
|
+
// same derivation), one scope (the user's configuration root), one entry per
|
|
4
|
+
// subsystem carrying the resolved executable invocation and the contract major
|
|
5
|
+
// it must answer. No search path, no fallback chain, no PATH scan: the
|
|
6
|
+
// executable is derived from the one root the owner declared, and every
|
|
7
|
+
// substantive call performs the `--version --json` handshake first.
|
|
8
|
+
//
|
|
9
|
+
// Handshake results carry no path — a status is machine JSON a consumer may
|
|
10
|
+
// print, and the manifest itself is the only place a location is written.
|
|
11
|
+
import { statSync } from 'node:fs';
|
|
12
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
export const SUBSYSTEMS_SCHEMA_VERSION = 1;
|
|
16
|
+
export const MEMORY_CONTRACT_MAJOR = 1;
|
|
17
|
+
const MEMORY_EXECUTABLE_REL = ['shared-memory', 'memory.mjs'];
|
|
18
|
+
const HANDSHAKE_TIMEOUT_MS = 15_000;
|
|
19
|
+
export class SubsystemsError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'SubsystemsError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Exactly one location per platform. An unset root is refused, not guessed:
|
|
29
|
+
* a manifest written under a guessed home is a manifest nothing else reads.
|
|
30
|
+
*/
|
|
31
|
+
export function subsystemsManifestPath(env, platform) {
|
|
32
|
+
if (platform === 'win32') {
|
|
33
|
+
const root = env.APPDATA;
|
|
34
|
+
if (!root)
|
|
35
|
+
throw new SubsystemsError('config-root-unavailable', 'APPDATA is not set');
|
|
36
|
+
return path.join(root, 'create-agent-rig', 'subsystems.json');
|
|
37
|
+
}
|
|
38
|
+
const home = env.HOME;
|
|
39
|
+
if (!home)
|
|
40
|
+
throw new SubsystemsError('config-root-unavailable', 'HOME is not set');
|
|
41
|
+
return path.join(home, '.config', 'create-agent-rig', 'subsystems.json');
|
|
42
|
+
}
|
|
43
|
+
/** The path module of the DECLARED platform, so an entry's encoding is pinned by the platform argument, not by the host. */
|
|
44
|
+
const pathFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
|
|
45
|
+
export function memoryExecutablePath(memoryRoot, platform) {
|
|
46
|
+
return pathFor(platform).join(memoryRoot, ...MEMORY_EXECUTABLE_REL);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The pure half of the derivation: root → invocation, in the declared
|
|
50
|
+
* platform's encoding. The root is the single declared input
|
|
51
|
+
* (`setup --memory-root`), so it must be absolute under that platform — a
|
|
52
|
+
* relative root would make the entry mean a different executable from every
|
|
53
|
+
* working directory. No filesystem here, so both encodings are pinned by tests
|
|
54
|
+
* on any host.
|
|
55
|
+
*/
|
|
56
|
+
export function memoryInvocation(input, platform) {
|
|
57
|
+
if (!pathFor(platform).isAbsolute(input.memoryRoot))
|
|
58
|
+
throw new SubsystemsError('memory-root-relative', 'the Memory root must be an absolute path');
|
|
59
|
+
return [input.nodeExecutable, memoryExecutablePath(input.memoryRoot, platform)];
|
|
60
|
+
}
|
|
61
|
+
function isFile(file) {
|
|
62
|
+
try {
|
|
63
|
+
return statSync(file).isFile();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The whole derivation: the invocation above, plus the check that the
|
|
71
|
+
* executable is already there on this host — the manifest records what
|
|
72
|
+
* exists, it does not promise what an install will bring. So `platform` is
|
|
73
|
+
* the host's here; the pure half is what a foreign platform's encoding is
|
|
74
|
+
* tested through.
|
|
75
|
+
*/
|
|
76
|
+
export function deriveMemoryEntry(input, platform) {
|
|
77
|
+
const invocation = memoryInvocation(input, platform);
|
|
78
|
+
if (!isFile(invocation[1]))
|
|
79
|
+
throw new SubsystemsError('memory-executable-absent', `no ${MEMORY_EXECUTABLE_REL.join('/')} under the declared Memory root`);
|
|
80
|
+
return {
|
|
81
|
+
memoryRoot: input.memoryRoot,
|
|
82
|
+
invocation,
|
|
83
|
+
contractMajor: MEMORY_CONTRACT_MAJOR,
|
|
84
|
+
memoryRef: input.memoryRef,
|
|
85
|
+
installedVersion: input.installedVersion,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const majorOf = (contractVersion) => {
|
|
89
|
+
const match = /^(\d+)\.\d+$/.exec(contractVersion);
|
|
90
|
+
return match ? Number(match[1]) : null;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* `--version --json` and its classification. The consumer compares the
|
|
94
|
+
* contract major alone; a foreign major is the consumer's refusal (exit 4 in
|
|
95
|
+
* `setup`), never something Memory emits. A failure payload is
|
|
96
|
+
* INTEGRATION-FAILED, never absence: the executable answered.
|
|
97
|
+
*/
|
|
98
|
+
export async function handshake(entry, run, options = {}) {
|
|
99
|
+
const [file, script] = entry.invocation;
|
|
100
|
+
// Absence is the runner's answer (ENOENT), not a pre-check: the executable
|
|
101
|
+
// the manifest names is exactly what this call must exercise.
|
|
102
|
+
const result = await run(file, [script, '--version', '--json'], {
|
|
103
|
+
timeoutMs: options.timeoutMs ?? HANDSHAKE_TIMEOUT_MS,
|
|
104
|
+
});
|
|
105
|
+
if (result.spawnError) {
|
|
106
|
+
if (result.spawnError.code === 'ENOENT')
|
|
107
|
+
return { status: 'unsupported', reason: 'absent' };
|
|
108
|
+
return { status: 'integration-failed', reason: 'invalid-payload' };
|
|
109
|
+
}
|
|
110
|
+
let payload;
|
|
111
|
+
try {
|
|
112
|
+
payload = JSON.parse(result.stdout);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { status: 'integration-failed', reason: 'invalid-payload' };
|
|
116
|
+
}
|
|
117
|
+
if (typeof payload !== 'object' || payload === null)
|
|
118
|
+
return { status: 'integration-failed', reason: 'invalid-payload' };
|
|
119
|
+
const record = payload;
|
|
120
|
+
// The executable answered, and answered a failure — a broken VERSION file is
|
|
121
|
+
// the Memory-side shape (RP-19). The manifest's promise about what is
|
|
122
|
+
// installed no longer holds, so this is `manifest-stale`, never absence.
|
|
123
|
+
if (result.code !== 0 || record.result === 'integration-failed')
|
|
124
|
+
return { status: 'integration-failed', reason: 'manifest-stale' };
|
|
125
|
+
const { name, version, contractVersion } = record;
|
|
126
|
+
if (typeof version !== 'string' || typeof contractVersion !== 'string')
|
|
127
|
+
return { status: 'integration-failed', reason: 'invalid-payload' };
|
|
128
|
+
if (name !== 'memory')
|
|
129
|
+
return { status: 'integration-failed', reason: 'manifest-stale' };
|
|
130
|
+
const major = majorOf(contractVersion);
|
|
131
|
+
if (major === null)
|
|
132
|
+
return { status: 'integration-failed', reason: 'manifest-stale' };
|
|
133
|
+
if (major !== MEMORY_CONTRACT_MAJOR)
|
|
134
|
+
return { status: 'foreign-major', contractVersion, requiredMajor: MEMORY_CONTRACT_MAJOR };
|
|
135
|
+
return { status: 'ok', version, contractVersion };
|
|
136
|
+
}
|
|
137
|
+
/** Stable bytes: fixed key order, two-space indent, trailing newline. */
|
|
138
|
+
export function serializeSubsystemsManifest(manifest) {
|
|
139
|
+
const memory = manifest.entries.memory;
|
|
140
|
+
const ordered = {
|
|
141
|
+
schemaVersion: SUBSYSTEMS_SCHEMA_VERSION,
|
|
142
|
+
entries: {
|
|
143
|
+
memory: {
|
|
144
|
+
memoryRoot: memory.memoryRoot,
|
|
145
|
+
invocation: [memory.invocation[0], memory.invocation[1]],
|
|
146
|
+
contractMajor: MEMORY_CONTRACT_MAJOR,
|
|
147
|
+
memoryRef: memory.memoryRef,
|
|
148
|
+
installedVersion: memory.installedVersion,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
return `${JSON.stringify(ordered, null, 2)}\n`;
|
|
153
|
+
}
|
|
154
|
+
const isString = (value) => typeof value === 'string';
|
|
155
|
+
export function parseSubsystemsManifest(text) {
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = JSON.parse(text);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
164
|
+
return null;
|
|
165
|
+
const root = parsed;
|
|
166
|
+
if (root.schemaVersion !== SUBSYSTEMS_SCHEMA_VERSION)
|
|
167
|
+
return null;
|
|
168
|
+
const entries = root.entries;
|
|
169
|
+
if (typeof entries !== 'object' || entries === null)
|
|
170
|
+
return null;
|
|
171
|
+
const memory = entries.memory;
|
|
172
|
+
if (typeof memory !== 'object' || memory === null)
|
|
173
|
+
return null;
|
|
174
|
+
const m = memory;
|
|
175
|
+
const invocation = m.invocation;
|
|
176
|
+
if (!isString(m.memoryRoot) ||
|
|
177
|
+
!Array.isArray(invocation) ||
|
|
178
|
+
invocation.length !== 2 ||
|
|
179
|
+
!invocation.every(isString) ||
|
|
180
|
+
m.contractMajor !== MEMORY_CONTRACT_MAJOR ||
|
|
181
|
+
!(m.memoryRef === null || isString(m.memoryRef)) ||
|
|
182
|
+
!isString(m.installedVersion))
|
|
183
|
+
return null;
|
|
184
|
+
return {
|
|
185
|
+
schemaVersion: SUBSYSTEMS_SCHEMA_VERSION,
|
|
186
|
+
entries: {
|
|
187
|
+
memory: {
|
|
188
|
+
memoryRoot: m.memoryRoot,
|
|
189
|
+
// `every(isString)` above proved both elements are strings, but
|
|
190
|
+
// Array.prototype.every does not narrow a tuple's element type.
|
|
191
|
+
invocation: [invocation[0], invocation[1]],
|
|
192
|
+
contractMajor: MEMORY_CONTRACT_MAJOR,
|
|
193
|
+
// Checked above as `null` or a string; the `||` chain does not narrow `m.memoryRef`.
|
|
194
|
+
memoryRef: m.memoryRef,
|
|
195
|
+
installedVersion: m.installedVersion,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** `null` when absent — no evidence; a present file that does not parse is an error, not silence. */
|
|
201
|
+
export async function readSubsystemsManifest(file) {
|
|
202
|
+
let text;
|
|
203
|
+
try {
|
|
204
|
+
text = await readFile(file, 'utf8');
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error.code === 'ENOENT')
|
|
208
|
+
return null;
|
|
209
|
+
throw new SubsystemsError('manifest-unreadable', `cannot read ${path.basename(file)}`);
|
|
210
|
+
}
|
|
211
|
+
const manifest = parseSubsystemsManifest(text);
|
|
212
|
+
if (manifest === null)
|
|
213
|
+
throw new SubsystemsError('manifest-unreadable', `${path.basename(file)} is not a schemaVersion ${SUBSYSTEMS_SCHEMA_VERSION} subsystem manifest`);
|
|
214
|
+
return manifest;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Atomic and idempotent: the bytes land in a sibling temp file and are renamed
|
|
218
|
+
* over the target, so a reader never sees a torn manifest and a re-run of the
|
|
219
|
+
* same derivation produces no diff of its own.
|
|
220
|
+
*/
|
|
221
|
+
export async function writeSubsystemsManifest(file, manifest) {
|
|
222
|
+
const dir = path.dirname(file);
|
|
223
|
+
await mkdir(dir, { recursive: true });
|
|
224
|
+
const temp = path.join(dir, `.${path.basename(file)}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`);
|
|
225
|
+
try {
|
|
226
|
+
await writeFile(temp, serializeSubsystemsManifest(manifest));
|
|
227
|
+
await rename(temp, file);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// See packages/cli/test/subsystems.test.ts › "removes its temp file and
|
|
231
|
+
// rethrows when the rename is refused".
|
|
232
|
+
await rm(temp, { force: true });
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* What `upgrade` re-runs: the same derivation from the recorded root, the same
|
|
238
|
+
* handshake, and a rewrite only when the handshake is ok. A manifest that is
|
|
239
|
+
* absent is left absent (`setup` is the one writer that creates it); one whose
|
|
240
|
+
* executable no longer answers is left untouched and the result says why.
|
|
241
|
+
*/
|
|
242
|
+
export async function refreshSubsystems(options) {
|
|
243
|
+
const manifest = await readSubsystemsManifest(options.file);
|
|
244
|
+
if (manifest === null)
|
|
245
|
+
return 'absent';
|
|
246
|
+
const previous = manifest.entries.memory;
|
|
247
|
+
let entry;
|
|
248
|
+
try {
|
|
249
|
+
entry = deriveMemoryEntry({
|
|
250
|
+
memoryRoot: previous.memoryRoot,
|
|
251
|
+
nodeExecutable: options.nodeExecutable,
|
|
252
|
+
memoryRef: previous.memoryRef,
|
|
253
|
+
installedVersion: previous.installedVersion,
|
|
254
|
+
}, options.platform);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof SubsystemsError && error.code === 'memory-executable-absent')
|
|
258
|
+
return { status: 'unsupported', reason: 'absent' };
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
const result = await handshake(entry, options.run);
|
|
262
|
+
if (result.status !== 'ok')
|
|
263
|
+
return result;
|
|
264
|
+
await writeSubsystemsManifest(options.file, {
|
|
265
|
+
schemaVersion: SUBSYSTEMS_SCHEMA_VERSION,
|
|
266
|
+
entries: { memory: { ...entry, installedVersion: result.version } },
|
|
267
|
+
});
|
|
268
|
+
return 'refreshed';
|
|
269
|
+
}
|
|
@@ -13,3 +13,18 @@ export async function packageVersion() {
|
|
|
13
13
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
14
14
|
return pkg.version;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The version of the command contract (`docs/command-contract.md`) this rig
|
|
18
|
+
* implements. Independent of the package version: many releases ship against
|
|
19
|
+
* one contract, and a consumer compares this field's major alone.
|
|
20
|
+
*/
|
|
21
|
+
export const RIG_CONTRACT_VERSION = '1.0';
|
|
22
|
+
/** What `--version --json` answers — the handshake object of the contract. */
|
|
23
|
+
export async function rigHandshake() {
|
|
24
|
+
return {
|
|
25
|
+
schemaVersion: 1,
|
|
26
|
+
name: 'create-agent-rig',
|
|
27
|
+
version: await packageVersion(),
|
|
28
|
+
contractVersion: RIG_CONTRACT_VERSION,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { CAPABILITY_STATES } from '../core/vocabulary.js';
|
|
2
|
+
export const BENCHMARK_CLASSIFICATIONS = Object.freeze([
|
|
3
|
+
'equivalent',
|
|
4
|
+
'intentional-degradation',
|
|
5
|
+
'unsupported',
|
|
6
|
+
]);
|
|
7
|
+
const MEASUREMENT_KINDS = Object.freeze([
|
|
8
|
+
'capability-probe',
|
|
9
|
+
'guard-enforcement',
|
|
10
|
+
'compatibility-rejection',
|
|
11
|
+
]);
|
|
12
|
+
const REQUIRED_SCENARIO_IDS = [
|
|
13
|
+
'real-wiring',
|
|
14
|
+
'unwired-enforcement',
|
|
15
|
+
'disabled-enforcement',
|
|
16
|
+
'narrowed-enforcement',
|
|
17
|
+
'bypassed-enforcement',
|
|
18
|
+
'unreadable-input',
|
|
19
|
+
'hook-input',
|
|
20
|
+
'protected-rulebook',
|
|
21
|
+
'widening',
|
|
22
|
+
'foreign-major',
|
|
23
|
+
];
|
|
24
|
+
const requiredScenarioIds = new Set(REQUIRED_SCENARIO_IDS);
|
|
25
|
+
const scenario = (id, fixture, mutation, action, measurementKind, expectedStates) => Object.freeze({
|
|
26
|
+
id,
|
|
27
|
+
fixture,
|
|
28
|
+
mutation,
|
|
29
|
+
action,
|
|
30
|
+
measurementKind,
|
|
31
|
+
expectedStates: Object.freeze([...expectedStates]),
|
|
32
|
+
});
|
|
33
|
+
export const BENCHMARK_CORPUS = Object.freeze({
|
|
34
|
+
contractMajor: 1,
|
|
35
|
+
scenarios: Object.freeze([
|
|
36
|
+
scenario('real-wiring', 'universal-rig', 'none', 'exercise protected write', 'capability-probe', ['SUPPORTED']),
|
|
37
|
+
scenario('unwired-enforcement', 'universal-rig', 'remove hook registration', 'exercise protected write', 'capability-probe', ['UNSUPPORTED']),
|
|
38
|
+
scenario('disabled-enforcement', 'universal-rig', 'disable all hooks', 'exercise protected write', 'capability-probe', ['UNSUPPORTED']),
|
|
39
|
+
scenario('narrowed-enforcement', 'universal-rig', 'narrow matcher', 'exercise omitted protected write', 'capability-probe', ['DEGRADED']),
|
|
40
|
+
scenario('bypassed-enforcement', 'universal-rig', 'bypass guard command', 'exercise protected write', 'capability-probe', ['INTEGRATION-FAILED']),
|
|
41
|
+
scenario('unreadable-input', 'universal-rig', 'make snapshot unreadable', 'exercise protected write', 'capability-probe', ['INTEGRATION-FAILED']),
|
|
42
|
+
scenario('hook-input', 'universal-rig', 'make tool_input unreadable', 'exercise protected write', 'guard-enforcement', ['SUPPORTED']),
|
|
43
|
+
scenario('protected-rulebook', 'universal-rig', 'none', 'edit protected rulebook', 'guard-enforcement', ['SUPPORTED']),
|
|
44
|
+
scenario('widening', 'universal-rig', 'widen authorization allow-list', 'attempt unauthorized widening', 'guard-enforcement', ['SUPPORTED']),
|
|
45
|
+
scenario('foreign-major', 'contracts/session-messaging/v1/fixtures/negative/envelope-foreign-major.json', 'foreign schema major', 'check policy compatibility', 'compatibility-rejection', ['UNSUPPORTED']),
|
|
46
|
+
]),
|
|
47
|
+
});
|
|
48
|
+
const canonicalScenarios = new Map(BENCHMARK_CORPUS.scenarios.map((entry) => [entry.id, entry]));
|
|
49
|
+
export const classifyCapability = (state) => {
|
|
50
|
+
switch (state) {
|
|
51
|
+
case 'SUPPORTED':
|
|
52
|
+
return { classification: 'equivalent', integrationFailed: false };
|
|
53
|
+
case 'DEGRADED':
|
|
54
|
+
return { classification: 'intentional-degradation', integrationFailed: false };
|
|
55
|
+
case 'UNSUPPORTED':
|
|
56
|
+
return { classification: 'unsupported', integrationFailed: false };
|
|
57
|
+
case 'INTEGRATION-FAILED':
|
|
58
|
+
return { classification: 'unsupported', integrationFailed: true };
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Unknown capability state: ${String(state)}`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const isOwnDataRecord = (value) => {
|
|
64
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
65
|
+
return false;
|
|
66
|
+
const prototype = Object.getPrototypeOf(value);
|
|
67
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
68
|
+
return false;
|
|
69
|
+
return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => descriptor.enumerable === true && 'value' in descriptor);
|
|
70
|
+
};
|
|
71
|
+
const isDataArray = (value) => {
|
|
72
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype)
|
|
73
|
+
return false;
|
|
74
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
75
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
76
|
+
if (descriptor === undefined || descriptor.enumerable !== true || !('value' in descriptor))
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
const hasOnlyKeys = (value, keys) => {
|
|
82
|
+
const actual = Object.keys(value);
|
|
83
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
84
|
+
};
|
|
85
|
+
const nonBlank = (value) => typeof value === 'string' && value.trim() !== '';
|
|
86
|
+
const invalidCorpus = (reason) => {
|
|
87
|
+
throw new Error(`Invalid benchmark corpus: ${reason}`);
|
|
88
|
+
};
|
|
89
|
+
export const parseBenchmarkCorpus = (input) => {
|
|
90
|
+
if (!isOwnDataRecord(input) || !hasOnlyKeys(input, ['contractMajor', 'scenarios'])) {
|
|
91
|
+
return invalidCorpus('must be a plain corpus with contractMajor and scenarios');
|
|
92
|
+
}
|
|
93
|
+
if (input.contractMajor !== 1)
|
|
94
|
+
return invalidCorpus('unsupported contract major; expected 1');
|
|
95
|
+
if (!isDataArray(input.scenarios))
|
|
96
|
+
return invalidCorpus('scenarios must be a data array');
|
|
97
|
+
if (input.scenarios.length !== REQUIRED_SCENARIO_IDS.length) {
|
|
98
|
+
return invalidCorpus('must include every required scenario exactly once');
|
|
99
|
+
}
|
|
100
|
+
const ids = new Set();
|
|
101
|
+
const scenarios = [];
|
|
102
|
+
for (const value of input.scenarios) {
|
|
103
|
+
if (!isOwnDataRecord(value) ||
|
|
104
|
+
!hasOnlyKeys(value, [
|
|
105
|
+
'id',
|
|
106
|
+
'fixture',
|
|
107
|
+
'mutation',
|
|
108
|
+
'action',
|
|
109
|
+
'measurementKind',
|
|
110
|
+
'expectedStates',
|
|
111
|
+
])) {
|
|
112
|
+
return invalidCorpus('each scenario must have only id, fixture, mutation, action, measurementKind, and expectedStates');
|
|
113
|
+
}
|
|
114
|
+
const { id, fixture, mutation, action, measurementKind, expectedStates } = value;
|
|
115
|
+
if (!nonBlank(id) || !requiredScenarioIds.has(id))
|
|
116
|
+
return invalidCorpus(`unknown scenario ${String(id)}`);
|
|
117
|
+
if (ids.has(id))
|
|
118
|
+
return invalidCorpus(`scenario ${id} is duplicated`);
|
|
119
|
+
if (!nonBlank(fixture) || !nonBlank(mutation) || !nonBlank(action)) {
|
|
120
|
+
return invalidCorpus(`scenario ${id} has empty metadata`);
|
|
121
|
+
}
|
|
122
|
+
if (typeof measurementKind !== 'string' ||
|
|
123
|
+
!MEASUREMENT_KINDS.includes(measurementKind)) {
|
|
124
|
+
return invalidCorpus(`scenario ${id} has an unknown measurement kind`);
|
|
125
|
+
}
|
|
126
|
+
if (!isDataArray(expectedStates) || expectedStates.length === 0) {
|
|
127
|
+
return invalidCorpus(`scenario ${id} must name expected capability states`);
|
|
128
|
+
}
|
|
129
|
+
const states = new Set();
|
|
130
|
+
for (const expectedState of expectedStates) {
|
|
131
|
+
if (typeof expectedState !== 'string' ||
|
|
132
|
+
!CAPABILITY_STATES.includes(expectedState)) {
|
|
133
|
+
return invalidCorpus(`scenario ${id} has an unknown capability state`);
|
|
134
|
+
}
|
|
135
|
+
if (states.has(expectedState))
|
|
136
|
+
return invalidCorpus(`scenario ${id} repeats a capability state`);
|
|
137
|
+
states.add(expectedState);
|
|
138
|
+
}
|
|
139
|
+
const canonical = canonicalScenarios.get(id);
|
|
140
|
+
if (fixture !== canonical.fixture ||
|
|
141
|
+
mutation !== canonical.mutation ||
|
|
142
|
+
action !== canonical.action ||
|
|
143
|
+
measurementKind !== canonical.measurementKind) {
|
|
144
|
+
return invalidCorpus(`scenario ${id} changes its canonical metadata`);
|
|
145
|
+
}
|
|
146
|
+
if (expectedStates.length !== canonical.expectedStates.length ||
|
|
147
|
+
expectedStates.some((state, index) => state !== canonical.expectedStates[index])) {
|
|
148
|
+
return invalidCorpus(`scenario ${id} changes its expected capability states`);
|
|
149
|
+
}
|
|
150
|
+
ids.add(id);
|
|
151
|
+
scenarios.push(Object.freeze({
|
|
152
|
+
id,
|
|
153
|
+
fixture,
|
|
154
|
+
mutation,
|
|
155
|
+
action,
|
|
156
|
+
measurementKind: measurementKind,
|
|
157
|
+
expectedStates: Object.freeze([...states]),
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
for (const id of REQUIRED_SCENARIO_IDS) {
|
|
161
|
+
if (!ids.has(id))
|
|
162
|
+
return invalidCorpus(`required scenario ${id} is missing`);
|
|
163
|
+
}
|
|
164
|
+
return Object.freeze({ contractMajor: 1, scenarios: Object.freeze(scenarios) });
|
|
165
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract a harness adapter satisfies (RP-76).
|
|
3
|
+
*
|
|
4
|
+
* The core knows that a harness exists and has a hook-wiring surface; it does
|
|
5
|
+
* not know any harness's name, event vocabulary, tool names or paths. Those
|
|
6
|
+
* belong to the adapter, one module per harness under `../harness/`, and
|
|
7
|
+
* adding a harness means adding one such module and registering it — pinned in
|
|
8
|
+
* `test/template/policy-declaration.test.ts` under "adding a harness touches
|
|
9
|
+
* adapters only", whose two tests name exactly the files that may mention each
|
|
10
|
+
* harness. Their names carry the harness words this file may not, which is why
|
|
11
|
+
* the describe is cited here rather than either test.
|
|
12
|
+
*
|
|
13
|
+
* An adapter is a mapping, not a compiler: given a declaration it names the
|
|
14
|
+
* event, matcher and hook path the harness wires for it. The correspondence
|
|
15
|
+
* between that answer and the snapshot the rig actually ships is a test, in
|
|
16
|
+
* both directions, in the same file.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|