create-agent-rig 0.9.1 → 0.10.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 +419 -3
- package/README.md +267 -305
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +472 -90
- package/packages/cli/dist/index.js +651 -46
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +64 -5
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +8 -4
- package/templates/release-ledger.json +2 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- package/templates/skeleton/node-service/vitest.config.ts +0 -12
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { isPlainObject, scanForDepthAndControlChars } from '../lib/safe-text.js';
|
|
2
|
+
export const DECLARATION_REL = '.rig/integrations.json';
|
|
3
|
+
export const DECLARATION_SCHEMA_VERSION = 1;
|
|
4
|
+
export const MAX_DECLARATION_BYTES = 64 * 1024;
|
|
5
|
+
export const VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
6
|
+
export const ROOT_KEYS = Object.freeze([
|
|
7
|
+
'schemaVersion',
|
|
8
|
+
'integrations',
|
|
9
|
+
'targets',
|
|
10
|
+
]);
|
|
11
|
+
export const KNOWN_ENTRY_KEYS = Object.freeze([
|
|
12
|
+
'id',
|
|
13
|
+
'required',
|
|
14
|
+
'version',
|
|
15
|
+
'harnesses',
|
|
16
|
+
'selected',
|
|
17
|
+
'targets',
|
|
18
|
+
]);
|
|
19
|
+
const hash = /^[0-9a-f]{64}$/;
|
|
20
|
+
export function truncateForMessage(value) {
|
|
21
|
+
return value.length > 64 ? `${value.slice(0, 64)}…` : value;
|
|
22
|
+
}
|
|
23
|
+
const isHarness = (value) => value === 'claude-code' || value === 'codex';
|
|
24
|
+
function rejectKey(entry) {
|
|
25
|
+
for (const key of Object.keys(entry)) {
|
|
26
|
+
if (!KNOWN_ENTRY_KEYS.includes(key))
|
|
27
|
+
return ['command', 'args', 'env', 'url', 'headers', 'source'].includes(key)
|
|
28
|
+
? 'arbitrary-command-refused'
|
|
29
|
+
: 'malformed';
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export function parseDeclaration(raw, registry) {
|
|
34
|
+
if (new TextEncoder().encode(raw).byteLength > MAX_DECLARATION_BYTES)
|
|
35
|
+
return { status: 'invalid', error: 'the declaration is larger than 64 KiB' };
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { status: 'invalid', error: 'the declaration is not valid JSON' };
|
|
42
|
+
}
|
|
43
|
+
const scan = scanForDepthAndControlChars(parsed, 6);
|
|
44
|
+
if (scan.tooDeep)
|
|
45
|
+
return {
|
|
46
|
+
status: 'invalid',
|
|
47
|
+
error: 'the declaration nests deeper than 6 levels below its root',
|
|
48
|
+
};
|
|
49
|
+
if (scan.hasControlChar)
|
|
50
|
+
return { status: 'invalid', error: 'the declaration carries a control or format character' };
|
|
51
|
+
if (!isPlainObject(parsed) ||
|
|
52
|
+
Object.keys(parsed).some((key) => !ROOT_KEYS.includes(key)) ||
|
|
53
|
+
parsed.schemaVersion !== 1 ||
|
|
54
|
+
!Array.isArray(parsed.integrations))
|
|
55
|
+
return { status: 'invalid', error: 'the declaration shape is invalid' };
|
|
56
|
+
let targets;
|
|
57
|
+
if (Object.hasOwn(parsed, 'targets')) {
|
|
58
|
+
if (!isPlainObject(parsed.targets) ||
|
|
59
|
+
Object.keys(parsed.targets).some((key) => key !== 'codex') ||
|
|
60
|
+
(Object.hasOwn(parsed.targets, 'codex') &&
|
|
61
|
+
(!isPlainObject(parsed.targets.codex) ||
|
|
62
|
+
Object.keys(parsed.targets.codex).length !== 1 ||
|
|
63
|
+
typeof parsed.targets.codex.fileHash !== 'string' ||
|
|
64
|
+
!hash.test(parsed.targets.codex.fileHash))))
|
|
65
|
+
return { status: 'invalid', error: 'the declaration targets shape is invalid' };
|
|
66
|
+
if (Object.hasOwn(parsed.targets, 'codex'))
|
|
67
|
+
targets = {
|
|
68
|
+
codex: { fileHash: parsed.targets.codex.fileHash },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const ids = new Set(), entries = [], rejected = [], known = new Set(registry.map((item) => item.id));
|
|
72
|
+
for (const value of parsed.integrations) {
|
|
73
|
+
if (!isPlainObject(value) ||
|
|
74
|
+
typeof value.id !== 'string' ||
|
|
75
|
+
value.id === '' ||
|
|
76
|
+
ids.has(value.id))
|
|
77
|
+
return { status: 'invalid', error: 'every declared integration needs a unique string "id"' };
|
|
78
|
+
ids.add(value.id);
|
|
79
|
+
const bad = rejectKey(value);
|
|
80
|
+
if (bad !== undefined) {
|
|
81
|
+
rejected.push({ id: value.id, reason: bad });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!known.has(value.id)) {
|
|
85
|
+
rejected.push({ id: value.id, reason: 'not-in-matrix' });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (value.selected !== true ||
|
|
89
|
+
(Object.hasOwn(value, 'required') && typeof value.required !== 'boolean') ||
|
|
90
|
+
(Object.hasOwn(value, 'version') &&
|
|
91
|
+
(typeof value.version !== 'string' || !VERSION_PATTERN.test(value.version)))) {
|
|
92
|
+
rejected.push({ id: value.id, reason: 'malformed' });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
let harnesses;
|
|
96
|
+
if (Object.hasOwn(value, 'harnesses')) {
|
|
97
|
+
if (!Array.isArray(value.harnesses) ||
|
|
98
|
+
value.harnesses.length === 0 ||
|
|
99
|
+
new Set(value.harnesses).size !== value.harnesses.length ||
|
|
100
|
+
!value.harnesses.every(isHarness)) {
|
|
101
|
+
rejected.push({ id: value.id, reason: 'malformed' });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
harnesses = value.harnesses;
|
|
105
|
+
}
|
|
106
|
+
let targets;
|
|
107
|
+
if (Object.hasOwn(value, 'targets')) {
|
|
108
|
+
if (!isPlainObject(value.targets)) {
|
|
109
|
+
rejected.push({ id: value.id, reason: 'malformed' });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
targets = {};
|
|
113
|
+
let badTarget = false;
|
|
114
|
+
for (const [name, target] of Object.entries(value.targets)) {
|
|
115
|
+
if (name !== 'claude-code' ||
|
|
116
|
+
!isPlainObject(target) ||
|
|
117
|
+
Object.keys(target).length !== 1 ||
|
|
118
|
+
typeof target.entryHash !== 'string' ||
|
|
119
|
+
!hash.test(target.entryHash)) {
|
|
120
|
+
badTarget = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
targets[name] = { entryHash: target.entryHash };
|
|
124
|
+
}
|
|
125
|
+
if (badTarget ||
|
|
126
|
+
(harnesses !== undefined &&
|
|
127
|
+
Object.keys(targets).some((name) => !harnesses.includes(name)))) {
|
|
128
|
+
rejected.push({
|
|
129
|
+
id: value.id,
|
|
130
|
+
reason: badTarget ? 'arbitrary-command-refused' : 'malformed',
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
entries.push({
|
|
136
|
+
id: value.id,
|
|
137
|
+
...(typeof value.required === 'boolean' ? { required: value.required } : {}),
|
|
138
|
+
...(value.version === undefined ? {} : { version: value.version }),
|
|
139
|
+
...(harnesses === undefined ? {} : { harnesses }),
|
|
140
|
+
selected: true,
|
|
141
|
+
...(targets === undefined ? {} : { targets }),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return { status: 'ok', entries, rejected, ...(targets === undefined ? {} : { targets }) };
|
|
145
|
+
}
|
|
146
|
+
export function serializeDeclaration(entries, targets) {
|
|
147
|
+
const integrations = [...entries]
|
|
148
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
149
|
+
.map((entry) => ({
|
|
150
|
+
id: entry.id,
|
|
151
|
+
...(entry.required === undefined ? {} : { required: entry.required }),
|
|
152
|
+
...(entry.version === undefined ? {} : { version: entry.version }),
|
|
153
|
+
...(entry.harnesses === undefined ? {} : { harnesses: entry.harnesses }),
|
|
154
|
+
selected: true,
|
|
155
|
+
...(entry.targets === undefined ? {} : { targets: entry.targets }),
|
|
156
|
+
}));
|
|
157
|
+
return `${JSON.stringify({ schemaVersion: 1, integrations, ...(targets === undefined ? {} : { targets }) }, null, 2)}\n`;
|
|
158
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { initFileContents } from '../commands/init.js';
|
|
2
|
+
import { MANIFEST_REL, parseManifest, sha256 } from '../lib/manifest.js';
|
|
3
|
+
import { readBounded } from './verify.js';
|
|
4
|
+
import { runProviderProcess } from './spawn.js';
|
|
5
|
+
import { agentOsUniversalDir } from '../templates.js';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
+
const MAX_BYTES = 1024 * 1024;
|
|
8
|
+
const WIRING = ['.claude/settings.json', '.codex/hooks.json'];
|
|
9
|
+
const failed = (reason) => ({
|
|
10
|
+
status: 'fail',
|
|
11
|
+
reason,
|
|
12
|
+
});
|
|
13
|
+
function text(bytes) {
|
|
14
|
+
try {
|
|
15
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function includesWiring(actual, expected) {
|
|
22
|
+
if (Array.isArray(expected))
|
|
23
|
+
return (Array.isArray(actual) &&
|
|
24
|
+
expected.every((entry) => actual.some((candidate) => includesWiring(candidate, entry))));
|
|
25
|
+
if (typeof expected !== 'object' || expected === null)
|
|
26
|
+
return actual === expected;
|
|
27
|
+
if (typeof actual !== 'object' || actual === null || Array.isArray(actual))
|
|
28
|
+
return false;
|
|
29
|
+
const wanted = expected;
|
|
30
|
+
const found = actual;
|
|
31
|
+
// A hook command must keep its complete behavior (including Windows command,
|
|
32
|
+
// synchronous execution and timeout). Foreign sibling entries remain allowed.
|
|
33
|
+
if ('command' in wanted)
|
|
34
|
+
return isDeepStrictEqual(actual, expected);
|
|
35
|
+
return Object.entries(wanted).every(([key, value]) => includesWiring(found[key], value));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fixed package-owned fixture runner. The payload consists only of bytes from
|
|
39
|
+
* `initFileContents`; it never loads the checked repository's hooks, settings,
|
|
40
|
+
* commands, or input. It makes an isolated root and runs the finite fixtures
|
|
41
|
+
* under a child process managed by the common safe-process boundary.
|
|
42
|
+
*/
|
|
43
|
+
const FIXTURE_WRAPPER = String.raw `
|
|
44
|
+
const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process'),url=require('node:url');
|
|
45
|
+
void (async()=>{
|
|
46
|
+
const source=process.argv[1];
|
|
47
|
+
const root=fs.mkdtempSync(path.join(os.tmpdir(),'rig-guard-fixtures-'));
|
|
48
|
+
const parent=path.resolve(os.tmpdir()),resolved=path.resolve(root);
|
|
49
|
+
if(!resolved.startsWith(parent+path.sep)||!path.basename(resolved).startsWith('rig-guard-fixtures-'))throw new Error('unsafe fixture root');
|
|
50
|
+
let code=1;
|
|
51
|
+
try {
|
|
52
|
+
const home=path.join(root,'home'); fs.mkdirSync(path.join(root,'.claude','agents'),{recursive:true});
|
|
53
|
+
fs.writeFileSync(path.join(root,'.claude','agents','code-reviewer.md'),'---\nmodel: gpt-5.6-sol\n---\n');
|
|
54
|
+
const env={...process.env,HOME:home,APPDATA:home,CLAUDE_PROJECT_DIR:root};
|
|
55
|
+
const unattended=path.join(source,'.claude','scripts','unattended-flag.mjs');
|
|
56
|
+
const flag=await import(url.pathToFileURL(unattended).href); flag.writeUnattended({item:'fixture',runDir:root,allow:[]},env);
|
|
57
|
+
const run=(rel,input)=>cp.spawnSync(process.execPath,[path.join(source,...rel.split('/'))],{cwd:root,env,input:JSON.stringify(input),encoding:'utf8',timeout:5000,maxBuffer:8192}).status;
|
|
58
|
+
const pre=(tool_input)=>({hook_event_name:'PreToolUse',tool_name:tool_input.command?'Bash':'Write',tool_input});
|
|
59
|
+
const cases=[
|
|
60
|
+
['.claude/hooks/guard-secret-file.mjs',pre({file_path:path.join(root,'.env'),content:'TOKEN=x'}),false],
|
|
61
|
+
['.claude/hooks/guard-secret-file.mjs',pre({file_path:path.join(root,'note.txt'),content:'safe'}),true],
|
|
62
|
+
['.claude/hooks/block-no-verify.mjs',pre({command:'git commit --no-verify'}),false],
|
|
63
|
+
['.claude/hooks/block-no-verify.mjs',pre({command:'git status'}),true],
|
|
64
|
+
['.claude/hooks/guard-bash.mjs',pre({command:'git push --force origin master'}),false],
|
|
65
|
+
['.claude/hooks/guard-bash.mjs',pre({command:'git status'}),true],
|
|
66
|
+
['.claude/hooks/guard-rulebook.mjs',{hook_event_name:'PreToolUse',tool_name:'Edit',tool_input:{file_path:path.join(root,'.claude','agents','code-reviewer.md'),new_string:'model: gpt-5.6-terra'}},false],
|
|
67
|
+
['.claude/hooks/guard-rulebook.mjs',pre({file_path:path.join(root,'note.txt'),content:'safe'}),true],
|
|
68
|
+
['.claude/hooks/guard-subagent-model.mjs',{hook_event_name:'PreToolUse',tool_name:'Agent',tool_input:{subagent_type:'code-reviewer',model:'haiku'}},false],
|
|
69
|
+
['.claude/hooks/guard-subagent-model.mjs',{hook_event_name:'PreToolUse',tool_name:'Agent',tool_input:{subagent_type:'code-reviewer'}},true]
|
|
70
|
+
];
|
|
71
|
+
for(const [rel,input,allowed] of cases){const status=run(rel,input);if(allowed ? status!==0 : status!==2){code=1;break;} code=0;}
|
|
72
|
+
} finally { fs.rmSync(resolved,{recursive:true,force:true}); }
|
|
73
|
+
process.exitCode=code;
|
|
74
|
+
})().catch(()=>{process.exitCode=1;});
|
|
75
|
+
`;
|
|
76
|
+
export async function inspectGuards(options) {
|
|
77
|
+
const manifestFile = await readBounded(options.repoDir, MANIFEST_REL, MAX_BYTES);
|
|
78
|
+
const manifest = manifestFile.status === 'ok' ? parseManifest(text(manifestFile.bytes) ?? '') : null;
|
|
79
|
+
if (manifest === null)
|
|
80
|
+
return failed('hook-wiring-invalid');
|
|
81
|
+
const expected = await initFileContents(options.repoDir, manifest.project, manifest.layers);
|
|
82
|
+
for (const rel of WIRING) {
|
|
83
|
+
const observed = await readBounded(options.repoDir, rel, MAX_BYTES);
|
|
84
|
+
const wanted = expected.get(rel);
|
|
85
|
+
const actualText = observed.status === 'ok' ? text(observed.bytes) : null;
|
|
86
|
+
const expectedText = wanted === undefined ? null : wanted;
|
|
87
|
+
if (actualText === null || expectedText === null)
|
|
88
|
+
return failed('hook-wiring-invalid');
|
|
89
|
+
let actual, generated;
|
|
90
|
+
try {
|
|
91
|
+
actual = JSON.parse(actualText);
|
|
92
|
+
generated = JSON.parse(expectedText);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return failed('hook-wiring-invalid');
|
|
96
|
+
}
|
|
97
|
+
const observedSettings = actual;
|
|
98
|
+
const expectedSettings = generated;
|
|
99
|
+
if (observedSettings?.disableAllHooks === true ||
|
|
100
|
+
!includesWiring(observedSettings?.hooks, expectedSettings.hooks))
|
|
101
|
+
return failed('hook-wiring-invalid');
|
|
102
|
+
}
|
|
103
|
+
const dependencies = [...expected.keys()].filter((rel) => rel.startsWith('.claude/hooks/') ||
|
|
104
|
+
rel.startsWith('.claude/scripts/lib/') ||
|
|
105
|
+
rel === '.claude/scripts/stop-flag.mjs' ||
|
|
106
|
+
rel === '.claude/scripts/unattended-flag.mjs' ||
|
|
107
|
+
rel === '.claude/scripts/git-env.mjs');
|
|
108
|
+
for (const rel of dependencies) {
|
|
109
|
+
const observed = await readBounded(options.repoDir, rel, MAX_BYTES);
|
|
110
|
+
const wanted = expected.get(rel);
|
|
111
|
+
if (observed.status !== 'ok' ||
|
|
112
|
+
wanted === undefined ||
|
|
113
|
+
sha256(observed.bytes) !== sha256(Buffer.from(wanted)))
|
|
114
|
+
return failed('hook-integrity-invalid');
|
|
115
|
+
}
|
|
116
|
+
const batch = await (options.runner ?? runProviderProcess)({
|
|
117
|
+
executable: process.execPath,
|
|
118
|
+
args: ['-e', FIXTURE_WRAPPER, agentOsUniversalDir()],
|
|
119
|
+
repoDir: options.repoDir,
|
|
120
|
+
timeoutMs: 30_000,
|
|
121
|
+
maxOutputBytes: 8 * 1024,
|
|
122
|
+
});
|
|
123
|
+
if (batch.status !== 'ok' || batch.exitCode !== 0)
|
|
124
|
+
return failed('guard-fixture-batch-failed');
|
|
125
|
+
return { status: 'pass', reason: 'guards-verified' };
|
|
126
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { initFileContents } from '../commands/init.js';
|
|
2
|
+
import { MANIFEST_REL, parseManifest, sha256 } from '../lib/manifest.js';
|
|
3
|
+
import { readBounded } from './verify.js';
|
|
4
|
+
export async function inspectWorkflow(options) {
|
|
5
|
+
const failed = { status: 'fail', reason: 'workflow-integrity-invalid' };
|
|
6
|
+
const source = await readBounded(options.repoDir, MANIFEST_REL, 1024 * 1024);
|
|
7
|
+
if (source.status !== 'ok')
|
|
8
|
+
return failed;
|
|
9
|
+
const manifest = parseManifest(source.bytes.toString('utf8'));
|
|
10
|
+
if (manifest === null)
|
|
11
|
+
return failed;
|
|
12
|
+
if (!manifest.layers.includes('workflow'))
|
|
13
|
+
return { status: 'pass', reason: 'not-selected' };
|
|
14
|
+
const expected = await initFileContents(options.repoDir, manifest.project, manifest.layers);
|
|
15
|
+
// Package-owned mechanisms only. No tracker reads, claim rewrites, or execution
|
|
16
|
+
// of repository scripts: the frozen behavior is compared as installed bytes.
|
|
17
|
+
for (const [rel, content] of expected) {
|
|
18
|
+
if (!rel.startsWith('.claude/scripts/'))
|
|
19
|
+
continue;
|
|
20
|
+
const observed = await readBounded(options.repoDir, rel, 1024 * 1024);
|
|
21
|
+
if (observed.status !== 'ok' || sha256(observed.bytes) !== sha256(content))
|
|
22
|
+
return failed;
|
|
23
|
+
}
|
|
24
|
+
return { status: 'pass', reason: 'workflow-verified' };
|
|
25
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isPlainObject } from '../lib/safe-text.js';
|
|
2
|
+
const whitespace = (text, from) => {
|
|
3
|
+
while (' \t\r\n'.includes(text[from] ?? '\0'))
|
|
4
|
+
from++;
|
|
5
|
+
return from;
|
|
6
|
+
};
|
|
7
|
+
const stringEnd = (text, from) => {
|
|
8
|
+
for (let i = from + 1; i < text.length; i++) {
|
|
9
|
+
if (text[i] === '\\')
|
|
10
|
+
i++;
|
|
11
|
+
else if (text[i] === '"')
|
|
12
|
+
return i + 1;
|
|
13
|
+
}
|
|
14
|
+
throw new Error('mcp-config-invalid');
|
|
15
|
+
};
|
|
16
|
+
// JSON.parse validates the complete document first. This bounded, iterative
|
|
17
|
+
// walk only locates member spans; it never decodes or reserializes foreign values.
|
|
18
|
+
function valueEnd(text, from) {
|
|
19
|
+
if (text[from] === '"')
|
|
20
|
+
return stringEnd(text, from);
|
|
21
|
+
if (text[from] === '{' || text[from] === '[') {
|
|
22
|
+
let depth = 0;
|
|
23
|
+
for (let i = from; i < text.length; i++) {
|
|
24
|
+
if (text[i] === '"')
|
|
25
|
+
i = stringEnd(text, i) - 1;
|
|
26
|
+
else if (text[i] === '{' || text[i] === '[')
|
|
27
|
+
depth++;
|
|
28
|
+
else if ((text[i] === '}' || text[i] === ']') && --depth === 0)
|
|
29
|
+
return i + 1;
|
|
30
|
+
}
|
|
31
|
+
throw new Error('mcp-config-invalid');
|
|
32
|
+
}
|
|
33
|
+
let end = from;
|
|
34
|
+
while (end < text.length && !' \t\r\n,}]'.includes(text[end]))
|
|
35
|
+
end++;
|
|
36
|
+
return end;
|
|
37
|
+
}
|
|
38
|
+
function objectSpan(text, start) {
|
|
39
|
+
const members = [];
|
|
40
|
+
const keys = new Set();
|
|
41
|
+
let offset = whitespace(text, start + 1);
|
|
42
|
+
while (text[offset] !== '}') {
|
|
43
|
+
const endOfKey = stringEnd(text, offset);
|
|
44
|
+
const key = JSON.parse(text.slice(offset, endOfKey));
|
|
45
|
+
if (keys.has(key))
|
|
46
|
+
throw new Error('mcp-config-ambiguous');
|
|
47
|
+
keys.add(key);
|
|
48
|
+
const valueStart = whitespace(text, whitespace(text, endOfKey) + 1);
|
|
49
|
+
const end = valueEnd(text, valueStart);
|
|
50
|
+
const separator = whitespace(text, end);
|
|
51
|
+
const comma = text[separator] === ',' ? separator : undefined;
|
|
52
|
+
members.push({
|
|
53
|
+
key,
|
|
54
|
+
start: offset,
|
|
55
|
+
valueStart,
|
|
56
|
+
end,
|
|
57
|
+
...(comma === undefined ? {} : { comma }),
|
|
58
|
+
});
|
|
59
|
+
offset = comma === undefined ? separator : whitespace(text, comma + 1);
|
|
60
|
+
}
|
|
61
|
+
return { start, members };
|
|
62
|
+
}
|
|
63
|
+
function document(text) {
|
|
64
|
+
const value = JSON.parse(text);
|
|
65
|
+
if (!isPlainObject(value) ||
|
|
66
|
+
(Object.hasOwn(value, 'mcpServers') && !isPlainObject(value.mcpServers)))
|
|
67
|
+
throw new Error('mcp-config-invalid');
|
|
68
|
+
const root = objectSpan(text, whitespace(text, 0));
|
|
69
|
+
const servers = root.members.find((member) => member.key === 'mcpServers');
|
|
70
|
+
const serverSpan = servers === undefined ? undefined : objectSpan(text, servers.valueStart);
|
|
71
|
+
return { config: { ...value, mcpServers: value.mcpServers ?? {} }, root, serverSpan };
|
|
72
|
+
}
|
|
73
|
+
export function readMcpConfig(text) {
|
|
74
|
+
return document(text).config;
|
|
75
|
+
}
|
|
76
|
+
function editMember(text, span, key, value) {
|
|
77
|
+
const index = span.members.findIndex((member) => member.key === key);
|
|
78
|
+
const existing = span.members[index];
|
|
79
|
+
if (existing !== undefined) {
|
|
80
|
+
if (value !== undefined)
|
|
81
|
+
return text.slice(0, existing.valueStart) + JSON.stringify(value) + text.slice(existing.end);
|
|
82
|
+
const precedingComma = span.members[index - 1]?.comma;
|
|
83
|
+
const start = existing.comma !== undefined || precedingComma === undefined
|
|
84
|
+
? existing.start
|
|
85
|
+
: precedingComma;
|
|
86
|
+
const end = existing.comma === undefined ? existing.end : existing.comma + 1;
|
|
87
|
+
return text.slice(0, start) + text.slice(end);
|
|
88
|
+
}
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
return text;
|
|
91
|
+
// Prepend only owned text. In particular, retain the original leading
|
|
92
|
+
// whitespace before foreign members so removing this entry restores it.
|
|
93
|
+
const inserted = `${JSON.stringify(key)}:${JSON.stringify(value)}${span.members.length ? ',' : ''}`;
|
|
94
|
+
return text.slice(0, span.start + 1) + inserted + text.slice(span.start + 1);
|
|
95
|
+
}
|
|
96
|
+
export function editMcpServers(text, changes) {
|
|
97
|
+
for (const [key, value] of changes) {
|
|
98
|
+
const parsed = document(text);
|
|
99
|
+
text =
|
|
100
|
+
parsed.serverSpan === undefined
|
|
101
|
+
? value === undefined
|
|
102
|
+
? text
|
|
103
|
+
: editMember(text, parsed.root, 'mcpServers', { [key]: value })
|
|
104
|
+
: editMember(text, parsed.serverSpan, key, value);
|
|
105
|
+
}
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readBounded } from './verify.js';
|
|
3
|
+
import { runProviderProcess } from './spawn.js';
|
|
4
|
+
import { handshake, parseSubsystemsManifest, subsystemsManifestPath, } from '../lib/subsystems.js';
|
|
5
|
+
const MAX_MACHINE_MANIFEST_BYTES = 64 * 1024;
|
|
6
|
+
const result = (status, reason, runtime, contract) => ({ status, reason, runtime, contract });
|
|
7
|
+
function manifestLocation(env) {
|
|
8
|
+
try {
|
|
9
|
+
const file = subsystemsManifestPath(env, process.platform);
|
|
10
|
+
const root = process.platform === 'win32' ? env.APPDATA : env.HOME;
|
|
11
|
+
if (root === undefined || root === '')
|
|
12
|
+
return null;
|
|
13
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
14
|
+
if (rel === '' || rel.startsWith('../') || path.isAbsolute(rel))
|
|
15
|
+
return null;
|
|
16
|
+
return { root, rel };
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function bridge(runner, repoDir) {
|
|
23
|
+
return async (file, args, { timeoutMs }) => {
|
|
24
|
+
const process = await runner({ executable: file, args, repoDir, timeoutMs });
|
|
25
|
+
if (process.status === 'ok' || (process.status === 'failed' && process.exitCode !== null))
|
|
26
|
+
return { code: process.exitCode ?? 1, stdout: process.stdout, stderr: process.stderr };
|
|
27
|
+
if (process.status === 'failed')
|
|
28
|
+
return {
|
|
29
|
+
code: 1,
|
|
30
|
+
stdout: '',
|
|
31
|
+
stderr: '',
|
|
32
|
+
spawnError: Object.assign(new Error('provider unavailable'), { code: 'ENOENT' }),
|
|
33
|
+
};
|
|
34
|
+
return { code: 1, stdout: '', stderr: '' };
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The Memory boundary owns its doctor output. Rig accepts only the published
|
|
39
|
+
* v1 summary shape and never forwards its detail or fix text into Rig output.
|
|
40
|
+
*/
|
|
41
|
+
function parseDoctorSummary(stdout) {
|
|
42
|
+
let value;
|
|
43
|
+
try {
|
|
44
|
+
value = JSON.parse(stdout);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
50
|
+
return null;
|
|
51
|
+
const report = value;
|
|
52
|
+
if (report.schemaVersion !== 1 || !isDoctorStatus(report.status) || !Array.isArray(report.checks))
|
|
53
|
+
return null;
|
|
54
|
+
let expected = 'ok';
|
|
55
|
+
for (const check of report.checks) {
|
|
56
|
+
if (typeof check !== 'object' || check === null || Array.isArray(check))
|
|
57
|
+
return null;
|
|
58
|
+
const record = check;
|
|
59
|
+
if (typeof record.id !== 'string' ||
|
|
60
|
+
record.id === '' ||
|
|
61
|
+
!isDoctorStatus(record.status) ||
|
|
62
|
+
typeof record.detail !== 'string' ||
|
|
63
|
+
typeof record.fix !== 'string')
|
|
64
|
+
return null;
|
|
65
|
+
if (record.status === 'fail')
|
|
66
|
+
expected = 'fail';
|
|
67
|
+
else if (record.status === 'warn' && expected === 'ok')
|
|
68
|
+
expected = 'warn';
|
|
69
|
+
}
|
|
70
|
+
return report.status === expected ? report.status : null;
|
|
71
|
+
}
|
|
72
|
+
function isDoctorStatus(value) {
|
|
73
|
+
return value === 'ok' || value === 'warn' || value === 'fail';
|
|
74
|
+
}
|
|
75
|
+
/** Inspect only the registered machine-local Memory boundary; never provider data. */
|
|
76
|
+
export async function inspectMemory(options) {
|
|
77
|
+
const location = manifestLocation(options.env);
|
|
78
|
+
if (location === null)
|
|
79
|
+
return result('warn', 'machine-config-root-unavailable', 'unverified', 'unverified');
|
|
80
|
+
const source = await readBounded(location.root, location.rel, MAX_MACHINE_MANIFEST_BYTES);
|
|
81
|
+
if (source.status === 'absent')
|
|
82
|
+
return result('pass', 'not-configured', 'unverified', 'unverified');
|
|
83
|
+
if (source.status !== 'ok')
|
|
84
|
+
return result('fail', 'manifest-invalid', 'unverified', 'unverified');
|
|
85
|
+
let manifest;
|
|
86
|
+
try {
|
|
87
|
+
manifest = parseSubsystemsManifest(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(source.bytes));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
manifest = null;
|
|
91
|
+
}
|
|
92
|
+
if (manifest === null)
|
|
93
|
+
return result('fail', 'manifest-invalid', 'unverified', 'unverified');
|
|
94
|
+
const runner = options.runner ?? runProviderProcess;
|
|
95
|
+
const checked = await handshake(manifest.entries.memory, bridge(runner, options.repoDir));
|
|
96
|
+
if (checked.status === 'foreign-major')
|
|
97
|
+
return result('fail', 'foreign-contract', 'unverified', 'foreign');
|
|
98
|
+
if (checked.status === 'unsupported')
|
|
99
|
+
return result('warn', 'executable-missing', 'unverified', 'unverified');
|
|
100
|
+
if (checked.status !== 'ok')
|
|
101
|
+
return result('fail', 'handshake-failed', 'unverified', 'unverified');
|
|
102
|
+
const doctor = await runner({
|
|
103
|
+
executable: manifest.entries.memory.invocation[0],
|
|
104
|
+
args: [manifest.entries.memory.invocation[1], 'doctor', '--json'],
|
|
105
|
+
repoDir: options.repoDir,
|
|
106
|
+
});
|
|
107
|
+
if (doctor.status === 'ok') {
|
|
108
|
+
const summary = parseDoctorSummary(doctor.stdout);
|
|
109
|
+
if (summary === null)
|
|
110
|
+
return result('fail', 'doctor-invalid', 'unverified', 'compatible');
|
|
111
|
+
if (summary === 'ok')
|
|
112
|
+
return result('pass', 'doctor-ok', 'verified', 'compatible');
|
|
113
|
+
if (summary === 'warn')
|
|
114
|
+
return result('warn', 'doctor-warn', 'verified', 'compatible');
|
|
115
|
+
return result('fail', 'doctor-failed', 'verified', 'compatible');
|
|
116
|
+
}
|
|
117
|
+
if (doctor.status === 'timeout')
|
|
118
|
+
return result('fail', 'doctor-timeout', 'unverified', 'compatible');
|
|
119
|
+
if (doctor.status === 'cleanup-unconfirmed')
|
|
120
|
+
return result('fail', 'doctor-cleanup-unconfirmed', 'unverified', 'compatible');
|
|
121
|
+
if (doctor.status === 'failed' && doctor.exitCode === null)
|
|
122
|
+
return result('warn', 'executable-missing', 'unverified', 'compatible');
|
|
123
|
+
return result('fail', 'doctor-failed', 'verified', 'compatible');
|
|
124
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const REGISTRY = Object.freeze([
|
|
2
|
+
Object.freeze({
|
|
3
|
+
id: 'basic-memory',
|
|
4
|
+
displayName: 'Basic Memory (wiring-only preview)',
|
|
5
|
+
routes: Object.freeze({ 'claude-code': 'automatic', codex: 'automatic' }),
|
|
6
|
+
}),
|
|
7
|
+
Object.freeze({
|
|
8
|
+
id: 'spec-kit',
|
|
9
|
+
displayName: 'Spec Kit (upstream-managed)',
|
|
10
|
+
routes: Object.freeze({ 'claude-code': 'automatic', codex: 'automatic' }),
|
|
11
|
+
}),
|
|
12
|
+
Object.freeze({
|
|
13
|
+
id: 'figma-mcp',
|
|
14
|
+
displayName: 'Figma MCP',
|
|
15
|
+
routes: Object.freeze({ 'claude-code': 'automatic', codex: 'automatic' }),
|
|
16
|
+
}),
|
|
17
|
+
Object.freeze({
|
|
18
|
+
id: 'atlassian-mcp',
|
|
19
|
+
displayName: 'Atlassian MCP',
|
|
20
|
+
routes: Object.freeze({ 'claude-code': 'automatic', codex: 'automatic' }),
|
|
21
|
+
}),
|
|
22
|
+
]);
|