alp-code 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 +770 -0
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/alp.config.yaml +5 -0
- package/dist/src/agents/agent-definition.js +28 -0
- package/dist/src/agents/capability-catalog.js +33 -0
- package/dist/src/agents/compaction.js +36 -0
- package/dist/src/agents/errors.js +12 -0
- package/dist/src/agents/librarian.js +38 -0
- package/dist/src/agents/main.js +37 -0
- package/dist/src/agents/memory-grant.js +29 -0
- package/dist/src/agents/model-context.js +70 -0
- package/dist/src/agents/modes.js +134 -0
- package/dist/src/agents/oracle.js +36 -0
- package/dist/src/agents/read-thread.js +38 -0
- package/dist/src/agents/registry.js +238 -0
- package/dist/src/agents/render-identity.js +38 -0
- package/dist/src/agents/review.js +37 -0
- package/dist/src/agents/search.js +37 -0
- package/dist/src/agents/shared/house-rules.js +33 -0
- package/dist/src/agents/shared/principal.js +18 -0
- package/dist/src/agents/shared/voice.js +29 -0
- package/dist/src/agents/titling.js +32 -0
- package/dist/src/agents/types.js +15 -0
- package/dist/src/backend/execution-backend.js +2 -0
- package/dist/src/backend/local-execution-store.js +144 -0
- package/dist/src/backend/local-process-backend.js +533 -0
- package/dist/src/backend/local-supervisor.js +104 -0
- package/dist/src/cli/alp.js +380 -0
- package/dist/src/cli/commands/context.js +203 -0
- package/dist/src/cli/commands/delegate.js +136 -0
- package/dist/src/cli/commands/identity-sync.js +31 -0
- package/dist/src/cli/commands/init.js +184 -0
- package/dist/src/cli/commands/mode.js +22 -0
- package/dist/src/cli/commands/principal.js +114 -0
- package/dist/src/cli/commands/run-main.js +90 -0
- package/dist/src/cli/commands/runtime.js +21 -0
- package/dist/src/cli/mode-preference-store.js +62 -0
- package/dist/src/cli/mode-selector.js +178 -0
- package/dist/src/cli/update-check.js +77 -0
- package/dist/src/context/checkpoint.js +134 -0
- package/dist/src/context/compact-journal.js +153 -0
- package/dist/src/context/compact-payload.js +121 -0
- package/dist/src/context/continuity.js +70 -0
- package/dist/src/context/types.js +2 -0
- package/dist/src/delegation/backend-registry.js +40 -0
- package/dist/src/delegation/delegation-service.js +300 -0
- package/dist/src/delegation/types.js +12 -0
- package/dist/src/execution/execution-policy.js +96 -0
- package/dist/src/execution/execution-service.js +115 -0
- package/dist/src/execution/execution-store.js +78 -0
- package/dist/src/execution/identity-capsule.js +65 -0
- package/dist/src/execution/types.js +12 -0
- package/dist/src/hooks/execution-bridge.js +84 -0
- package/dist/src/index.js +4 -0
- package/dist/src/memory/adapters/markdown-file-store.js +257 -0
- package/dist/src/memory/adapters/memory-api-client.js +2 -0
- package/dist/src/memory/adapters/memory-path-mapper.js +76 -0
- package/dist/src/memory/adapters/remote-api-store.js +25 -0
- package/dist/src/memory/context-ranker.js +21 -0
- package/dist/src/memory/errors.js +58 -0
- package/dist/src/memory/memory-service.js +149 -0
- package/dist/src/memory/memory-store.js +2 -0
- package/dist/src/memory/types.js +2 -0
- package/dist/src/policy/capability-policy.js +29 -0
- package/dist/src/policy/delegation-policy.js +25 -0
- package/dist/src/policy/errors.js +10 -0
- package/dist/src/policy/invariants.js +31 -0
- package/dist/src/policy/memory-policy.js +22 -0
- package/dist/src/policy/policy-engine.js +85 -0
- package/dist/src/policy/types.js +8 -0
- package/dist/src/policy/workspace-policy.js +77 -0
- package/dist/src/principal/principal-profile-store.js +89 -0
- package/dist/src/runtime/adapter-files.js +147 -0
- package/dist/src/runtime/claude-adapter.js +177 -0
- package/dist/src/runtime/codex-adapter.js +169 -0
- package/dist/src/runtime/permission-rules.js +156 -0
- package/dist/src/runtime/render-session-context.js +124 -0
- package/dist/src/runtime/render-task-input.js +33 -0
- package/dist/src/runtime/runtime-adapter.js +2 -0
- package/dist/src/runtime/runtime-preference-store.js +66 -0
- package/dist/src/runtime/runtime-selector.js +178 -0
- package/dist/src/runtime/types.js +2 -0
- package/dist/src/runtime/windows-shim.js +57 -0
- package/dist/src/state-paths.js +49 -0
- package/dist/src/workflow/output-validator.js +27 -0
- package/dist/src/workflow/repair-policy.js +8 -0
- package/dist/src/workflow/types.js +22 -0
- package/dist/src/workflow/workflow-runner.js +81 -0
- package/hooks/compact-record.cjs +109 -0
- package/hooks/session-boot.cjs +112 -0
- package/hooks/session-end.cjs +34 -0
- package/package.json +48 -0
- package/scaffold/memory/INDEX.md +27 -0
- package/scaffold/memory/README.md +76 -0
- package/scaffold/memory/projects/INDEX.md +22 -0
- package/scaffold/memory/projects/PROTOCOL.md +128 -0
- package/scaffold/memory/projects/_template/PROJECT.md +45 -0
- package/scripts/alp.cjs +126 -0
- package/scripts/alp.ps1 +4 -0
- package/scripts/alp.sh +3 -0
- package/scripts/bootstrap.cjs +144 -0
- package/scripts/checkout-release.cjs +30 -0
- package/scripts/delegate.cjs +19 -0
- package/scripts/doctor.cjs +158 -0
- package/scripts/doctor.sh +3 -0
- package/scripts/ensure-state.cjs +22 -0
- package/scripts/lib/cli-link.cjs +375 -0
- package/scripts/lib/codex-role.cjs +18 -0
- package/scripts/lib/delegation/command-runner.cjs +108 -0
- package/scripts/lib/delegation/config.cjs +81 -0
- package/scripts/lib/install-paths.cjs +154 -0
- package/scripts/lib/release-manifest.cjs +42 -0
- package/scripts/lib/semver-lite.cjs +20 -0
- package/scripts/lib/state.cjs +274 -0
- package/scripts/lib/uninstall.cjs +252 -0
- package/scripts/lib/update-check-worker.cjs +21 -0
- package/scripts/lib/update.cjs +395 -0
- package/scripts/run-role.cjs +42 -0
- package/scripts/run-role.ps1 +4 -0
- package/scripts/run-role.sh +3 -0
- package/scripts/sync-project-index.sh +167 -0
- package/skills/agent-memory/SKILL.md +109 -0
- package/skills/alp-debug/SKILL.md +90 -0
- package/skills/alp-debug/references/defense-in-depth.md +118 -0
- package/skills/alp-debug/references/investigation-methodology.md +106 -0
- package/skills/alp-debug/references/log-and-ci-analysis.md +96 -0
- package/skills/alp-debug/references/performance-diagnostics.md +112 -0
- package/skills/alp-debug/references/reporting-standards.md +120 -0
- package/skills/alp-debug/references/root-cause-tracing.md +134 -0
- package/skills/alp-debug/references/systematic-debugging.md +93 -0
- package/skills/alp-debug/references/verification.md +86 -0
- package/skills/alp-debug/scripts/find-polluter.sh +63 -0
- package/skills/alp-debug/scripts/find-polluter.test.md +102 -0
- package/skills/alp-plan/SKILL.md +128 -0
- package/skills/alp-plan/references/archive-workflow.md +77 -0
- package/skills/alp-plan/references/codebase-understanding.md +55 -0
- package/skills/alp-plan/references/output-standards.md +96 -0
- package/skills/alp-plan/references/plan-organization.md +129 -0
- package/skills/alp-plan/references/red-team-personas.md +76 -0
- package/skills/alp-plan/references/red-team-workflow.md +81 -0
- package/skills/alp-plan/references/research-phase.md +57 -0
- package/skills/alp-plan/references/scope-challenge.md +82 -0
- package/skills/alp-plan/references/solution-design.md +76 -0
- package/skills/alp-plan/references/validate-question-framework.md +89 -0
- package/skills/alp-plan/references/validate-workflow.md +83 -0
- package/skills/alp-predict/SKILL.md +98 -0
- package/skills/alp-scenario/SKILL.md +86 -0
- package/skills/code-review/SKILL.md +111 -0
- package/skills/code-review/references/code-review-reception.md +114 -0
- package/skills/code-review/references/edge-case-scouting.md +78 -0
- package/skills/code-review/references/verification-before-completion.md +117 -0
- package/skills/delegation/SKILL.md +46 -0
- package/skills/docs-seeker/.env.example +15 -0
- package/skills/docs-seeker/SKILL.md +87 -0
- package/skills/docs-seeker/package.json +25 -0
- package/skills/docs-seeker/references/advanced.md +82 -0
- package/skills/docs-seeker/references/context7-patterns.md +68 -0
- package/skills/docs-seeker/references/errors.md +72 -0
- package/skills/docs-seeker/scripts/analyze-llms-txt.js +211 -0
- package/skills/docs-seeker/scripts/detect-topic.js +172 -0
- package/skills/docs-seeker/scripts/fetch-docs.js +213 -0
- package/skills/docs-seeker/scripts/tests/run-tests.js +72 -0
- package/skills/docs-seeker/scripts/tests/test-analyze-llms.js +119 -0
- package/skills/docs-seeker/scripts/tests/test-detect-topic.js +112 -0
- package/skills/docs-seeker/scripts/tests/test-fetch-docs.js +84 -0
- package/skills/docs-seeker/scripts/utils/env-loader.js +94 -0
- package/skills/docs-seeker/workflows/library-search.md +73 -0
- package/skills/docs-seeker/workflows/repo-analysis.md +90 -0
- package/skills/docs-seeker/workflows/topic-search.md +69 -0
- package/skills/git/SKILL.md +121 -0
- package/skills/git/references/branch-management.md +90 -0
- package/skills/git/references/commit-standards.md +82 -0
- package/skills/git/references/gh-cli-guide.md +132 -0
- package/skills/git/references/safety-protocols.md +86 -0
- package/skills/git/references/workflow-commit.md +89 -0
- package/skills/git/references/workflow-merge.md +63 -0
- package/skills/git/references/workflow-pr.md +70 -0
- package/skills/git/references/workflow-push.md +62 -0
- package/skills/gkg/SKILL.md +87 -0
- package/skills/gkg/references/cli-commands.md +92 -0
- package/skills/gkg/references/http-api.md +99 -0
- package/skills/gkg/references/language-support.md +54 -0
- package/skills/problem-solving/SKILL.md +86 -0
- package/skills/problem-solving/references/attribution.md +48 -0
- package/skills/problem-solving/references/collision-zone-thinking.md +71 -0
- package/skills/problem-solving/references/inversion-exercise.md +88 -0
- package/skills/problem-solving/references/meta-pattern-recognition.md +80 -0
- package/skills/problem-solving/references/scale-game.md +82 -0
- package/skills/problem-solving/references/simplification-cascades.md +83 -0
- package/skills/problem-solving/references/when-stuck.md +76 -0
- package/skills/repomix/SKILL.md +94 -0
- package/skills/repomix/references/configuration.md +134 -0
- package/skills/repomix/references/usage-patterns.md +106 -0
- package/skills/repomix/scripts/.coverage +0 -0
- package/skills/repomix/scripts/README.md +179 -0
- package/skills/repomix/scripts/repomix_batch.py +455 -0
- package/skills/repomix/scripts/repos.example.json +15 -0
- package/skills/repomix/scripts/requirements.txt +15 -0
- package/skills/repomix/scripts/tests/test_repomix_batch.py +531 -0
- package/skills/research/SKILL.md +107 -0
- package/skills/security-scan/SKILL.md +101 -0
- package/skills/security-scan/references/secret-patterns.md +75 -0
- package/skills/security-scan/references/vulnerability-patterns.md +136 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRuntimeCommand = resolveRuntimeCommand;
|
|
4
|
+
exports.atomicRuntimeFile = atomicRuntimeFile;
|
|
5
|
+
exports.writeRuntimeContextFiles = writeRuntimeContextFiles;
|
|
6
|
+
exports.taskArguments = taskArguments;
|
|
7
|
+
exports.baseRuntimeEnvironment = baseRuntimeEnvironment;
|
|
8
|
+
exports.compactBridgeEnabled = compactBridgeEnabled;
|
|
9
|
+
exports.runtimeSkillRoots = runtimeSkillRoots;
|
|
10
|
+
exports.hookCommand = hookCommand;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const render_session_context_1 = require("./render-session-context");
|
|
14
|
+
const render_task_input_1 = require("./render-task-input");
|
|
15
|
+
let sequence = 0;
|
|
16
|
+
const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a runtime CLI's real command name on PATH. Windows installs of `claude`/`codex`
|
|
19
|
+
* are `.cmd` from npm, `.exe` from winget/native installers, or anything else PATHEXT
|
|
20
|
+
* lists — hardcoding `.cmd` misses every non-npm install even though it's on PATH and
|
|
21
|
+
* working (e.g. this very Claude Code session). Returns null when nothing on PATH matches,
|
|
22
|
+
* so callers can fall back to a platform-appropriate default for error messages.
|
|
23
|
+
*/
|
|
24
|
+
async function resolveRuntimeCommand(name, platform, env) {
|
|
25
|
+
const directories = (env.PATH ?? env.Path ?? "").split(node_path_1.delimiter).filter(Boolean);
|
|
26
|
+
if (platform !== "win32") {
|
|
27
|
+
for (const directory of directories) {
|
|
28
|
+
try {
|
|
29
|
+
await (0, promises_1.access)((0, node_path_1.join)(directory, name));
|
|
30
|
+
return name;
|
|
31
|
+
}
|
|
32
|
+
catch { /* continue */ }
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const extensions = (env.PATHEXT || DEFAULT_PATHEXT).split(";").map((value) => value.trim()).filter(Boolean);
|
|
37
|
+
for (const directory of directories) {
|
|
38
|
+
for (const extension of extensions) {
|
|
39
|
+
const candidate = name + extension;
|
|
40
|
+
try {
|
|
41
|
+
await (0, promises_1.access)((0, node_path_1.join)(directory, candidate));
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
catch { /* continue */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
async function atomicRuntimeFile(path, content) {
|
|
50
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
|
|
51
|
+
const temporary = (0, node_path_1.join)((0, node_path_1.dirname)(path), `.${(0, node_path_1.basename)(path)}.${process.pid}.${++sequence}.tmp`);
|
|
52
|
+
try {
|
|
53
|
+
await (0, promises_1.writeFile)(temporary, content, { encoding: "utf8", mode: 0o600 });
|
|
54
|
+
await (0, promises_1.rename)(temporary, path);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await (0, promises_1.rm)(temporary, { force: true });
|
|
58
|
+
}
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Writes the two context files every runtime needs, and decides which of them exists.
|
|
63
|
+
*
|
|
64
|
+
* Both adapters share this because the split is a property of ALP, not of a harness:
|
|
65
|
+
* session context describes the agent for the whole session and must never create a turn;
|
|
66
|
+
* task input *is* a turn, so it exists only where ALP is the one starting the conversation.
|
|
67
|
+
* Interactive therefore gets no task file at all — there is nothing for an adapter to
|
|
68
|
+
* accidentally hand to the CLI as a positional prompt.
|
|
69
|
+
*/
|
|
70
|
+
async function writeRuntimeContextFiles(execution, interactive) {
|
|
71
|
+
const { capsule, policy, artifacts } = execution;
|
|
72
|
+
const sessionContextFile = await atomicRuntimeFile((0, node_path_1.join)(artifacts.runtimeDirectory, "session-context.md"), (0, render_session_context_1.renderSessionContext)(capsule, policy));
|
|
73
|
+
const taskText = interactive ? null : (0, render_task_input_1.renderTaskInput)(capsule);
|
|
74
|
+
const taskFile = taskText === null
|
|
75
|
+
? null
|
|
76
|
+
: await atomicRuntimeFile((0, node_path_1.join)(artifacts.runtimeDirectory, "task.md"), taskText);
|
|
77
|
+
return Object.freeze({ sessionContextFile, taskFile, taskText });
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The positional argument that turns a task into the session's first turn.
|
|
81
|
+
*
|
|
82
|
+
* Empty for an interactive launch. That emptiness is the whole point of the split, so it is
|
|
83
|
+
* expressed once here rather than repeated as a conditional in each adapter.
|
|
84
|
+
*
|
|
85
|
+
* A path is only a task for a role that may open it. `titling` holds no tools at all and
|
|
86
|
+
* every other role's file reads are ACL-gated, so pointing such a role at `task.md` makes
|
|
87
|
+
* its first move the one thing it cannot do. It gets the task itself instead — the file is
|
|
88
|
+
* still written, because it is the artefact of record for the execution.
|
|
89
|
+
*/
|
|
90
|
+
function taskArguments(files, policy) {
|
|
91
|
+
if (files.taskFile === null || files.taskText === null)
|
|
92
|
+
return Object.freeze([]);
|
|
93
|
+
return Object.freeze(policy.allowedTools.includes("Read")
|
|
94
|
+
? [`ALP task is in ${files.taskFile}; execute it.`]
|
|
95
|
+
: [files.taskText]);
|
|
96
|
+
}
|
|
97
|
+
function baseRuntimeEnvironment(capsule, files, artifacts) {
|
|
98
|
+
return {
|
|
99
|
+
ALP_ROLE: capsule.role,
|
|
100
|
+
ALP_DELEGATED_ROLE: capsule.role,
|
|
101
|
+
ALP_DELEGATION_EXECUTION_ID: capsule.executionId,
|
|
102
|
+
ALP_DELEGATION_WORKSPACE: capsule.activeWorkspace,
|
|
103
|
+
// Read by `hooks/session-boot.cjs`. Taking it here rather than in each adapter is what
|
|
104
|
+
// stops a runtime from being wired up without it and silently falling back to the
|
|
105
|
+
// static role document, which carries no invariants or policy context.
|
|
106
|
+
ALP_SESSION_CONTEXT: files.sessionContextFile,
|
|
107
|
+
// Bound to the capsule's own policy, not read from a file: this is what
|
|
108
|
+
// `compact-record.cjs` stamps onto every journal line, and what `alp context` checks a
|
|
109
|
+
// checkpoint against before trusting it (invariant 5).
|
|
110
|
+
ALP_POLICY_HASH: capsule.policyHash,
|
|
111
|
+
// Read by `session-boot.cjs` on every SessionStart, `source="compact"` included — the
|
|
112
|
+
// continuity bridge's reinjection channel (plan §8.6).
|
|
113
|
+
ALP_CONTINUITY_CONTEXT: artifacts.continuityFile,
|
|
114
|
+
// Read only by `compact-record.cjs`; unconditional here because the checkpoint and its
|
|
115
|
+
// rendering exist regardless of `ALP_COMPACT_BRIDGE` — only the PreCompact/PostCompact
|
|
116
|
+
// hook registration is gated on that flag (see `compactBridgeEnabled`).
|
|
117
|
+
ALP_COMPACT_EVENTS: artifacts.compactEventsFile,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Whether this launch should register the PreCompact/PostCompact hooks. Gated on an
|
|
122
|
+
* environment variable rather than a runtime capability check: rollback is unsetting one
|
|
123
|
+
* variable, and Stage 4 of the rollout (plan §18) changes only the default returned here.
|
|
124
|
+
*/
|
|
125
|
+
function compactBridgeEnabled(env) {
|
|
126
|
+
return env.ALP_COMPACT_BRIDGE === "1";
|
|
127
|
+
}
|
|
128
|
+
function runtimeSkillRoots(env) {
|
|
129
|
+
const home = env.HOME ?? env.USERPROFILE;
|
|
130
|
+
const roots = [
|
|
131
|
+
...(env.ALP_SKILL_ROOTS ?? "").split(node_path_1.delimiter),
|
|
132
|
+
env.ALP_REPO_ROOT ? (0, node_path_1.join)(env.ALP_REPO_ROOT, "skills") : "",
|
|
133
|
+
home ? (0, node_path_1.join)(home, ".agents", "skills") : "",
|
|
134
|
+
env.CODEX_HOME ? (0, node_path_1.join)(env.CODEX_HOME, "skills") : home ? (0, node_path_1.join)(home, ".codex", "skills") : "",
|
|
135
|
+
env.CLAUDE_CONFIG_DIR ? (0, node_path_1.join)(env.CLAUDE_CONFIG_DIR, "skills") : home ? (0, node_path_1.join)(home, ".claude", "skills") : "",
|
|
136
|
+
].filter(Boolean).map((value) => (0, node_path_1.resolve)(value));
|
|
137
|
+
return [...new Set(roots)].join(node_path_1.delimiter);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Quotes for a shell, not for JSON. `JSON.stringify` escapes backslashes, so a Windows
|
|
141
|
+
* path came out as `C:\\Users\\...` inside the command — tolerated by path resolution,
|
|
142
|
+
* but wrong, and unreadable in the settings file. Plain double quotes are enough: these
|
|
143
|
+
* are paths we generate ourselves, and neither Windows nor our layout admits a `"`.
|
|
144
|
+
*/
|
|
145
|
+
function hookCommand(script, nodeExecutable = process.execPath) {
|
|
146
|
+
return `"${nodeExecutable}" "${script}"`;
|
|
147
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClaudeRuntimeAdapter = void 0;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const model_context_1 = require("../agents/model-context");
|
|
6
|
+
const registry_1 = require("../agents/registry");
|
|
7
|
+
const state_paths_1 = require("../state-paths");
|
|
8
|
+
const adapter_files_1 = require("./adapter-files");
|
|
9
|
+
const permission_rules_1 = require("./permission-rules");
|
|
10
|
+
class ClaudeRuntimeAdapter {
|
|
11
|
+
name = "claude";
|
|
12
|
+
/**
|
|
13
|
+
* Measured on Claude Code 2.1.259 (schema, 2026-09-03) and 2.1.240 (live, 2026-09-04):
|
|
14
|
+
* headless on darwin, and inherited TTY on win32 with `trigger=manual`. Both runs put
|
|
15
|
+
* reinjection inside the compaction:
|
|
16
|
+
*
|
|
17
|
+
* PreCompact -> SessionStart(source="compact") -> PostCompact
|
|
18
|
+
*
|
|
19
|
+
* so `PostCompact` is not a safe place to learn that a compaction finished, and a
|
|
20
|
+
* `PreCompact` with no `PostCompact` after it is ordinary — one was measured.
|
|
21
|
+
*/
|
|
22
|
+
compact = Object.freeze({
|
|
23
|
+
preCompact: true,
|
|
24
|
+
postCompact: true,
|
|
25
|
+
sessionStartAfterCompact: true,
|
|
26
|
+
});
|
|
27
|
+
platform;
|
|
28
|
+
env;
|
|
29
|
+
hooksDirectory;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.platform = options.platform ?? process.platform;
|
|
32
|
+
this.env = options.env ?? process.env;
|
|
33
|
+
this.hooksDirectory = options.hooksDirectory ?? (0, node_path_1.join)(this.env.ALP_REPO_ROOT ?? process.cwd(), "hooks");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Claude Code does not activate its filesystem sandbox on Windows — it reports the
|
|
37
|
+
* feature gate as off and, because ALP asks for `failIfUnavailable`, refuses to start at
|
|
38
|
+
* all. Requesting a sandbox that cannot exist turns every delegated execution on Windows
|
|
39
|
+
* into a startup failure, so the request is made only where it can be honoured. The
|
|
40
|
+
* read-only guarantee is not dropped with it: `claudePermissions` withdraws `Bash`
|
|
41
|
+
* instead, since a shell is the only remaining way such a role could write.
|
|
42
|
+
*/
|
|
43
|
+
sandboxAvailable() {
|
|
44
|
+
return this.platform !== "win32";
|
|
45
|
+
}
|
|
46
|
+
memoryRoot() {
|
|
47
|
+
return (0, state_paths_1.memoryRoot)(this.env);
|
|
48
|
+
}
|
|
49
|
+
async probe() {
|
|
50
|
+
const resolved = await (0, adapter_files_1.resolveRuntimeCommand)("claude", this.platform, this.env);
|
|
51
|
+
const command = resolved ?? (this.platform === "win32" ? "claude.cmd" : "claude");
|
|
52
|
+
return resolved
|
|
53
|
+
? { ok: true, runtime: this.name, message: `${command} available` }
|
|
54
|
+
: { ok: false, runtime: this.name, message: `${command} not found`, remediation: "Install Claude Code and ensure it is on PATH." };
|
|
55
|
+
}
|
|
56
|
+
async prepare(input) {
|
|
57
|
+
const { capsule, policy, artifacts } = input.execution;
|
|
58
|
+
const capsuleFile = await (0, adapter_files_1.atomicRuntimeFile)((0, node_path_1.join)(artifacts.runtimeDirectory, "identity-capsule.json"), `${JSON.stringify(capsule, null, 2)}\n`);
|
|
59
|
+
const contextFiles = await (0, adapter_files_1.writeRuntimeContextFiles)(input.execution, input.interactive);
|
|
60
|
+
const skillRoots = (0, adapter_files_1.runtimeSkillRoots)(this.env);
|
|
61
|
+
const autoCompactTokens = policy.autoCompactTokens[this.name] ?? (0, model_context_1.defaultAutoCompactTokens)(input.model);
|
|
62
|
+
const settingsFile = await (0, adapter_files_1.atomicRuntimeFile)((0, node_path_1.join)(artifacts.runtimeDirectory, "claude-settings.json"), `${JSON.stringify({
|
|
63
|
+
$schema: "https://json.schemastore.org/claude-code-settings.json",
|
|
64
|
+
alp: {
|
|
65
|
+
executionId: capsule.executionId,
|
|
66
|
+
policyHash: capsule.policyHash,
|
|
67
|
+
capsuleFile,
|
|
68
|
+
sessionContextFile: contextFiles.sessionContextFile,
|
|
69
|
+
...(contextFiles.taskFile === null ? {} : { taskFile: contextFiles.taskFile }),
|
|
70
|
+
},
|
|
71
|
+
// Ngưỡng vai khai, hoặc 90% cửa sổ của model khi vai không khai (§5.5). Chỉ vắng
|
|
72
|
+
// mặt khi ALP cũng không biết cửa sổ của model — lúc đó Claude giữ cửa sổ nó tự
|
|
73
|
+
// tune. Claude vẫn cap con số này theo cửa sổ context thật của phiên.
|
|
74
|
+
...(autoCompactTokens === null ? {} : { autoCompactWindow: autoCompactTokens }),
|
|
75
|
+
hooks: {
|
|
76
|
+
SessionStart: [{ hooks: [{ type: "command", command: (0, adapter_files_1.hookCommand)((0, node_path_1.join)(this.hooksDirectory, "session-boot.cjs")) }] }],
|
|
77
|
+
Stop: [{ hooks: [{ type: "command", command: (0, adapter_files_1.hookCommand)((0, node_path_1.join)(this.hooksDirectory, "session-end.cjs")) }] }],
|
|
78
|
+
// Gated on the flag (§10) and on the pinned capability — the latter only ever
|
|
79
|
+
// withholds a registration this build cannot back up with a measured event. No
|
|
80
|
+
// matcher: the measured `trigger` values are `manual`/`auto` and the bridge wants
|
|
81
|
+
// both.
|
|
82
|
+
...((0, adapter_files_1.compactBridgeEnabled)(this.env) && this.compact.preCompact ? {
|
|
83
|
+
PreCompact: [{ hooks: [{ type: "command", command: `${(0, adapter_files_1.hookCommand)((0, node_path_1.join)(this.hooksDirectory, "compact-record.cjs"))} pre claude` }] }],
|
|
84
|
+
} : {}),
|
|
85
|
+
...((0, adapter_files_1.compactBridgeEnabled)(this.env) && this.compact.postCompact ? {
|
|
86
|
+
PostCompact: [{ hooks: [{ type: "command", command: `${(0, adapter_files_1.hookCommand)((0, node_path_1.join)(this.hooksDirectory, "compact-record.cjs"))} post claude` }] }],
|
|
87
|
+
} : {}),
|
|
88
|
+
},
|
|
89
|
+
permissions: (0, permission_rules_1.claudePermissions)({
|
|
90
|
+
policy,
|
|
91
|
+
memoryRoot: this.memoryRoot(),
|
|
92
|
+
runtimeDirectory: artifacts.runtimeDirectory,
|
|
93
|
+
allRoles: registry_1.agentRegistry.list().map((definition) => definition.id),
|
|
94
|
+
sandboxed: this.sandboxAvailable(),
|
|
95
|
+
}),
|
|
96
|
+
...(policy.workspaceMode === "read-only" && this.sandboxAvailable() ? {
|
|
97
|
+
sandbox: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
failIfUnavailable: true,
|
|
100
|
+
allowUnsandboxedCommands: false,
|
|
101
|
+
filesystem: { denyWrite: [capsule.activeWorkspace] },
|
|
102
|
+
},
|
|
103
|
+
} : {}),
|
|
104
|
+
}, null, 2)}\n`);
|
|
105
|
+
/**
|
|
106
|
+
* Written for every execution, granted servers or none.
|
|
107
|
+
*
|
|
108
|
+
* The empty file is the point: paired with `--strict-mcp-config` it is what stops a
|
|
109
|
+
* delegated specialist from inheriting whatever MCP servers happen to be configured on
|
|
110
|
+
* this machine — egress no policy authorized and the Authority table never named.
|
|
111
|
+
*/
|
|
112
|
+
const mcpConfigFile = await (0, adapter_files_1.atomicRuntimeFile)((0, node_path_1.join)(artifacts.runtimeDirectory, "mcp-config.json"), `${JSON.stringify({
|
|
113
|
+
mcpServers: Object.fromEntries(policy.mcpServers.map((server) => [server.name, {
|
|
114
|
+
type: "stdio",
|
|
115
|
+
command: server.command,
|
|
116
|
+
args: [...server.args],
|
|
117
|
+
...(server.env === undefined ? {} : { env: server.env }),
|
|
118
|
+
}])),
|
|
119
|
+
}, null, 2)}\n`);
|
|
120
|
+
const skillRootsFile = await (0, adapter_files_1.atomicRuntimeFile)((0, node_path_1.join)(artifacts.runtimeDirectory, "skill-roots.json"), `${JSON.stringify(skillRoots.split(node_path_1.delimiter).filter(Boolean), null, 2)}\n`);
|
|
121
|
+
const env = {
|
|
122
|
+
...(0, adapter_files_1.baseRuntimeEnvironment)(capsule, contextFiles, artifacts),
|
|
123
|
+
ALP_EXECUTION_ROOT: (0, node_path_1.dirname)(artifacts.directory),
|
|
124
|
+
ALP_MEMORY_ROOT: this.memoryRoot(),
|
|
125
|
+
ALP_IDENTITY_CAPSULE: capsuleFile,
|
|
126
|
+
ALP_RUNTIME_CONFIG: settingsFile,
|
|
127
|
+
ALP_SKILL_ROOTS: skillRoots,
|
|
128
|
+
ALP_MODE: policy.mode,
|
|
129
|
+
...(policy.workspaceMode === "read-only" ? { ALP_READONLY_DIRS: capsule.activeWorkspace } : {}),
|
|
130
|
+
};
|
|
131
|
+
const command = (await (0, adapter_files_1.resolveRuntimeCommand)("claude", this.platform, this.env))
|
|
132
|
+
?? (this.platform === "win32" ? "claude.cmd" : "claude");
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
command,
|
|
135
|
+
args: Object.freeze([
|
|
136
|
+
"--settings", settingsFile,
|
|
137
|
+
"--model", input.model,
|
|
138
|
+
// Fail-closed MCP for a delegated execution. Interactive is the principal's own
|
|
139
|
+
// session at their own machine config — the same trade-off `--dangerously-skip-
|
|
140
|
+
// permissions` already makes one line below, and stated here so it is a decision
|
|
141
|
+
// rather than an omission.
|
|
142
|
+
"--mcp-config", mcpConfigFile,
|
|
143
|
+
...(input.interactive ? [] : ["--strict-mcp-config"]),
|
|
144
|
+
// Session-scoped subagent definitions (§4.6). No built-in holds a grant, so this is
|
|
145
|
+
// absent from every launch ALP makes today.
|
|
146
|
+
...(policy.subagents.length === 0 ? [] : ["--agents", JSON.stringify(Object.fromEntries(policy.subagents.map((subagent) => [subagent.name, {
|
|
147
|
+
description: subagent.description,
|
|
148
|
+
prompt: subagent.prompt,
|
|
149
|
+
tools: [...subagent.tools],
|
|
150
|
+
...(subagent.model === undefined ? {} : { model: subagent.model.claude }),
|
|
151
|
+
}])))]),
|
|
152
|
+
// Principal ngồi trước phiên interactive và tự duyệt được từng bước, nên prompt quyền chỉ
|
|
153
|
+
// là ma sát. Đánh đổi phải nói rõ: cờ này vô hiệu hoá `permissions.deny` ở settings trên —
|
|
154
|
+
// gồm cả cách ly private memory giữa các role, thứ chỉ Claude cưỡng chế được (§ACL trong
|
|
155
|
+
// permission-rules.ts). Chỉ `alp` (run-main) đặt interactive=true; `alp delegate` luôn
|
|
156
|
+
// false, nên specialist giữ nguyên sandbox và deny list.
|
|
157
|
+
...(input.interactive
|
|
158
|
+
? ["--dangerously-skip-permissions"]
|
|
159
|
+
: policy.workspaceMode === "read-only" ? ["--permission-mode", "plan"] : []),
|
|
160
|
+
// Empty when interactive: the principal's own first message is turn 1. Identity,
|
|
161
|
+
// invariants and policy have already arrived via the SessionStart hook.
|
|
162
|
+
...(0, adapter_files_1.taskArguments)(contextFiles, policy),
|
|
163
|
+
]),
|
|
164
|
+
cwd: capsule.activeWorkspace,
|
|
165
|
+
env: Object.freeze(env),
|
|
166
|
+
temporaryFiles: Object.freeze([
|
|
167
|
+
capsuleFile,
|
|
168
|
+
contextFiles.sessionContextFile,
|
|
169
|
+
...(contextFiles.taskFile === null ? [] : [contextFiles.taskFile]),
|
|
170
|
+
settingsFile,
|
|
171
|
+
mcpConfigFile,
|
|
172
|
+
skillRootsFile,
|
|
173
|
+
]),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
exports.ClaudeRuntimeAdapter = ClaudeRuntimeAdapter;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodexRuntimeAdapter = void 0;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const model_context_1 = require("../agents/model-context");
|
|
6
|
+
const registry_1 = require("../agents/registry");
|
|
7
|
+
const state_paths_1 = require("../state-paths");
|
|
8
|
+
const adapter_files_1 = require("./adapter-files");
|
|
9
|
+
const permission_rules_1 = require("./permission-rules");
|
|
10
|
+
class CodexRuntimeAdapter {
|
|
11
|
+
name = "codex";
|
|
12
|
+
/**
|
|
13
|
+
* Measured on Codex CLI 0.153.0 (schema 2026-09-03; live 2026-09-04 headless on darwin
|
|
14
|
+
* and win32, and inherited TTY on win32 with `trigger=manual`). Codex reinjects after it
|
|
15
|
+
* finishes, and only once the next turn begins:
|
|
16
|
+
*
|
|
17
|
+
* PreCompact -> PostCompact -> SessionStart(source="compact")
|
|
18
|
+
*
|
|
19
|
+
* measured at 7s past `PostCompact`, which was a human typing. Two earlier sessions
|
|
20
|
+
* exited straight after compacting, saw no `SessionStart`, and nearly had this pinned
|
|
21
|
+
* `false`. `manual` and `auto` payloads are identical apart from `trigger`.
|
|
22
|
+
*/
|
|
23
|
+
compact = Object.freeze({
|
|
24
|
+
preCompact: true,
|
|
25
|
+
postCompact: true,
|
|
26
|
+
sessionStartAfterCompact: true,
|
|
27
|
+
});
|
|
28
|
+
platform;
|
|
29
|
+
env;
|
|
30
|
+
hooksDirectory;
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.platform = options.platform ?? process.platform;
|
|
33
|
+
this.env = options.env ?? process.env;
|
|
34
|
+
this.hooksDirectory = options.hooksDirectory ?? (0, node_path_1.join)(this.env.ALP_REPO_ROOT ?? process.cwd(), "hooks");
|
|
35
|
+
}
|
|
36
|
+
memoryRoot() {
|
|
37
|
+
return (0, state_paths_1.memoryRoot)(this.env);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A hook command Codex can actually run, which on Windows is not the same string Claude
|
|
41
|
+
* Code needs.
|
|
42
|
+
*
|
|
43
|
+
* Codex splits the hook command into argv itself, and the first token may not be quoted:
|
|
44
|
+
* a command line that *starts with* `"` never resolves an executable. Measured on
|
|
45
|
+
* `codex-cli 0.153.0` (native `codex.exe`), both `"<node>" "<script>"` and the extra
|
|
46
|
+
* quote-pair form `""<node>" "<script>""` that v0.3.1 shipped report
|
|
47
|
+
* `hook: SessionStart Failed`, print nothing and leave the session with no identity —
|
|
48
|
+
* the pair theory was a `cmd /C` rule Codex turns out not to go through. Quoting the
|
|
49
|
+
* *later* arguments is fine: `<node> "<script>"` and `node "<script>"` both run, and the
|
|
50
|
+
* script path stays quoted because it can hold spaces.
|
|
51
|
+
*
|
|
52
|
+
* The interpreter therefore goes in bare. `process.execPath` is exact and used as-is when
|
|
53
|
+
* it has no space; when it does — `C:\Program Files\nodejs\node.exe`, the default install —
|
|
54
|
+
* the only bare spelling left is `node` off PATH, which the installed `alp.cmd` shim
|
|
55
|
+
* already depends on. Claude Code must NOT get this treatment: it spawns via
|
|
56
|
+
* `cmd /d /s /c "<command>"`, where the quoted form is what works today.
|
|
57
|
+
*/
|
|
58
|
+
hookCommand(script) {
|
|
59
|
+
const path = (0, node_path_1.join)(this.hooksDirectory, script);
|
|
60
|
+
if (this.platform !== "win32")
|
|
61
|
+
return (0, adapter_files_1.hookCommand)(path);
|
|
62
|
+
const node = process.execPath.includes(" ") ? "node" : process.execPath;
|
|
63
|
+
return `${node} "${path}"`;
|
|
64
|
+
}
|
|
65
|
+
async probe() {
|
|
66
|
+
const resolved = await (0, adapter_files_1.resolveRuntimeCommand)("codex", this.platform, this.env);
|
|
67
|
+
const command = resolved ?? (this.platform === "win32" ? "codex.cmd" : "codex");
|
|
68
|
+
return resolved
|
|
69
|
+
? { ok: true, runtime: this.name, message: `${command} available` }
|
|
70
|
+
: { ok: false, runtime: this.name, message: `${command} not found`, remediation: "Install Codex CLI and ensure it is on PATH." };
|
|
71
|
+
}
|
|
72
|
+
async prepare(input) {
|
|
73
|
+
const { capsule, policy, artifacts } = input.execution;
|
|
74
|
+
const capsuleFile = await (0, adapter_files_1.atomicRuntimeFile)((0, node_path_1.join)(artifacts.runtimeDirectory, "identity-capsule.json"), `${JSON.stringify(capsule, null, 2)}\n`);
|
|
75
|
+
const contextFiles = await (0, adapter_files_1.writeRuntimeContextFiles)(input.execution, input.interactive);
|
|
76
|
+
const skillRoots = (0, adapter_files_1.runtimeSkillRoots)(this.env);
|
|
77
|
+
const bootCommand = this.hookCommand("session-boot.cjs");
|
|
78
|
+
const stopCommand = this.hookCommand("session-end.cjs");
|
|
79
|
+
const bootHooks = `[{ hooks = [{ type = "command", command = ${(0, permission_rules_1.tomlString)(bootCommand)}, timeout = 30 }] }]`;
|
|
80
|
+
const stopHooks = `[{ hooks = [{ type = "command", command = ${(0, permission_rules_1.tomlString)(stopCommand)}, timeout = 30 }] }]`;
|
|
81
|
+
// Gated on the flag (§10), same as Claude — only the two events CB-0 measured as firing
|
|
82
|
+
// on this runtime. `manual` and `auto` were measured identical apart from `trigger`
|
|
83
|
+
// (plan §Runtime capability), so one registration covers both.
|
|
84
|
+
const bridgeEnabled = (0, adapter_files_1.compactBridgeEnabled)(this.env);
|
|
85
|
+
const preCompactCommand = `${this.hookCommand("compact-record.cjs")} pre codex`;
|
|
86
|
+
const postCompactCommand = `${this.hookCommand("compact-record.cjs")} post codex`;
|
|
87
|
+
const preCompactHooks = `[{ hooks = [{ type = "command", command = ${(0, permission_rules_1.tomlString)(preCompactCommand)}, timeout = 30 }] }]`;
|
|
88
|
+
const postCompactHooks = `[{ hooks = [{ type = "command", command = ${(0, permission_rules_1.tomlString)(postCompactCommand)}, timeout = 30 }] }]`;
|
|
89
|
+
const configFile = await (0, adapter_files_1.atomicRuntimeFile)((0, node_path_1.join)(artifacts.runtimeDirectory, "codex-config.toml"), [
|
|
90
|
+
`model = ${(0, permission_rules_1.tomlString)(input.model)}`,
|
|
91
|
+
`model_reasoning_effort = ${(0, permission_rules_1.tomlString)(input.reasoningEffort)}`,
|
|
92
|
+
`sandbox_mode = ${(0, permission_rules_1.tomlString)(policy.workspaceMode)}`,
|
|
93
|
+
...(0, permission_rules_1.codexSandboxLines)({
|
|
94
|
+
policy,
|
|
95
|
+
memoryRoot: this.memoryRoot(),
|
|
96
|
+
allRoles: registry_1.agentRegistry.list().map((definition) => definition.id),
|
|
97
|
+
// Unused here: Codex's sandbox restricts writes only, so reading the task file is
|
|
98
|
+
// never in question. Supplied because the grant is part of the shared contract,
|
|
99
|
+
// and making it optional is what let Claude ship without it.
|
|
100
|
+
runtimeDirectory: artifacts.runtimeDirectory,
|
|
101
|
+
}),
|
|
102
|
+
"[alp]",
|
|
103
|
+
`execution_id = ${(0, permission_rules_1.tomlString)(capsule.executionId)}`,
|
|
104
|
+
`capsule = ${(0, permission_rules_1.tomlString)(capsuleFile)}`,
|
|
105
|
+
`session_context = ${(0, permission_rules_1.tomlString)(contextFiles.sessionContextFile)}`,
|
|
106
|
+
...(contextFiles.taskFile === null ? [] : [`task = ${(0, permission_rules_1.tomlString)(contextFiles.taskFile)}`]),
|
|
107
|
+
"",
|
|
108
|
+
].join("\n"));
|
|
109
|
+
const skillRootsFile = await (0, adapter_files_1.atomicRuntimeFile)((0, node_path_1.join)(artifacts.runtimeDirectory, "skill-roots.json"), `${JSON.stringify(skillRoots.split(node_path_1.delimiter).filter(Boolean), null, 2)}\n`);
|
|
110
|
+
const autoCompactTokens = policy.autoCompactTokens[this.name] ?? (0, model_context_1.defaultAutoCompactTokens)(input.model);
|
|
111
|
+
const env = {
|
|
112
|
+
...(0, adapter_files_1.baseRuntimeEnvironment)(capsule, contextFiles, artifacts),
|
|
113
|
+
ALP_EXECUTION_ROOT: (0, node_path_1.dirname)(artifacts.directory),
|
|
114
|
+
ALP_MEMORY_ROOT: this.memoryRoot(),
|
|
115
|
+
ALP_IDENTITY_CAPSULE: capsuleFile,
|
|
116
|
+
ALP_RUNTIME_CONFIG: configFile,
|
|
117
|
+
ALP_SKILL_ROOTS: skillRoots,
|
|
118
|
+
ALP_MODE: policy.mode,
|
|
119
|
+
...(policy.workspaceMode === "read-only" ? { ALP_READONLY_DIRS: capsule.activeWorkspace } : {}),
|
|
120
|
+
};
|
|
121
|
+
const command = (await (0, adapter_files_1.resolveRuntimeCommand)("codex", this.platform, this.env))
|
|
122
|
+
?? (this.platform === "win32" ? "codex.cmd" : "codex");
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
command,
|
|
125
|
+
args: Object.freeze([
|
|
126
|
+
...(input.interactive ? [] : ["exec", "--skip-git-repo-check"]),
|
|
127
|
+
"--dangerously-bypass-hook-trust",
|
|
128
|
+
"--enable", "hooks",
|
|
129
|
+
"-C", capsule.activeWorkspace,
|
|
130
|
+
"-m", input.model,
|
|
131
|
+
"-c", `model_reasoning_effort=${(0, permission_rules_1.tomlString)(input.reasoningEffort)}`,
|
|
132
|
+
"-c", `hooks.SessionStart=${bootHooks}`,
|
|
133
|
+
"-c", `hooks.Stop=${stopHooks}`,
|
|
134
|
+
...(bridgeEnabled && this.compact.preCompact ? ["-c", `hooks.PreCompact=${preCompactHooks}`] : []),
|
|
135
|
+
...(bridgeEnabled && this.compact.postCompact ? ["-c", `hooks.PostCompact=${postCompactHooks}`] : []),
|
|
136
|
+
// Same reason the hooks ride here: `codex-config.toml` is ALP's file, not the one
|
|
137
|
+
// Codex loads. Vai không khai thì ALP tự tính 90% cửa sổ model — trùng đúng mặc định
|
|
138
|
+
// của Codex, nhưng tính ở đây thì Claude cũng nén ở cùng chỗ.
|
|
139
|
+
...(autoCompactTokens === null
|
|
140
|
+
? []
|
|
141
|
+
: ["-c", `model_auto_compact_token_limit=${autoCompactTokens}`]),
|
|
142
|
+
// Granted MCP servers. Codex has no in-process subagent, so a `subagents` grant is
|
|
143
|
+
// simply not translated here — §4.6: subagent là tối ưu hoá, không phải điều kiện.
|
|
144
|
+
...(0, permission_rules_1.codexMcpOverrides)(policy),
|
|
145
|
+
// Đối xứng với `--dangerously-skip-permissions` của Claude: phiên interactive bỏ approval
|
|
146
|
+
// và sandbox. `-s` bị bỏ đi chứ không để lẫn — Codex không báo lỗi khi có cả hai (chỉ
|
|
147
|
+
// `--approve-for-me` mới khai `conflicts_with`), cờ bypass thắng và `-s` thành dòng chết
|
|
148
|
+
// nói sai về chế độ đang chạy. Delegate luôn interactive=false nên vẫn đi nhánh `-s`.
|
|
149
|
+
...(input.interactive
|
|
150
|
+
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
151
|
+
: ["-s", policy.workspaceMode]),
|
|
152
|
+
// Measured on codex-cli 0.149.0: a positional PROMPT becomes a `role: user` message,
|
|
153
|
+
// i.e. turn 1. Interactive must not have one — identity reaches the model as a
|
|
154
|
+
// `role: developer` message from the SessionStart hook, ahead of the user's turn.
|
|
155
|
+
...(0, adapter_files_1.taskArguments)(contextFiles, policy),
|
|
156
|
+
]),
|
|
157
|
+
cwd: capsule.activeWorkspace,
|
|
158
|
+
env: Object.freeze(env),
|
|
159
|
+
temporaryFiles: Object.freeze([
|
|
160
|
+
capsuleFile,
|
|
161
|
+
contextFiles.sessionContextFile,
|
|
162
|
+
...(contextFiles.taskFile === null ? [] : [contextFiles.taskFile]),
|
|
163
|
+
configFile,
|
|
164
|
+
skillRootsFile,
|
|
165
|
+
]),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.CodexRuntimeAdapter = CodexRuntimeAdapter;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.absoluteRule = absoluteRule;
|
|
4
|
+
exports.claudePermissions = claudePermissions;
|
|
5
|
+
exports.tomlString = tomlString;
|
|
6
|
+
exports.codexSandboxLines = codexSandboxLines;
|
|
7
|
+
exports.codexMcpOverrides = codexMcpOverrides;
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const types_1 = require("../agents/types");
|
|
10
|
+
/**
|
|
11
|
+
* Claude Code reads an absolute path in a permission rule as relative to the directory
|
|
12
|
+
* holding the settings file unless it carries a DOUBLE leading slash. A single slash
|
|
13
|
+
* silently matches nothing, which disables the rule with no warning at all. Exactly two
|
|
14
|
+
* is the whole contract, so the path's own leading separator has to go: a POSIX path is
|
|
15
|
+
* already `/…`, and concatenating it after `//` yields three slashes — a rule that is
|
|
16
|
+
* just as silently dead as one slash.
|
|
17
|
+
*
|
|
18
|
+
* Exported so tests can assert against this format rather than restate it. The one
|
|
19
|
+
* assertion that did restate it agreed only on Windows, where `C:\…` carries no leading
|
|
20
|
+
* separator to strip, and so was red on every POSIX machine from the day it was written.
|
|
21
|
+
*/
|
|
22
|
+
function absoluteRule(verb, path) {
|
|
23
|
+
return `${verb}(//${path.replace(/\\/g, "/").replace(/^\/+/, "")}/**)`;
|
|
24
|
+
}
|
|
25
|
+
function claudePermissions(input) {
|
|
26
|
+
const { policy } = input;
|
|
27
|
+
const allow = [
|
|
28
|
+
...policy.skills.map((skill) => `Skill(${skill})`),
|
|
29
|
+
...policy.subagents.map((subagent) => `Agent(${subagent.name})`),
|
|
30
|
+
// Whole-server rule: `--strict-mcp-config` already means these are the only servers
|
|
31
|
+
// connected, so the grant is the server, not each tool it happens to expose.
|
|
32
|
+
...policy.mcpServers.map((server) => `mcp__${server.name}`),
|
|
33
|
+
];
|
|
34
|
+
const ownPrivate = (0, node_path_1.join)(input.memoryRoot, "private", policy.role);
|
|
35
|
+
const additionalDirectories = [
|
|
36
|
+
// A role with no declared root gets no read grant on the tree it happens to stand in.
|
|
37
|
+
// The ACL is the enforcement, so listing the workspace here would hand read-thread,
|
|
38
|
+
// compaction and titling exactly what their own instructions forbid.
|
|
39
|
+
...(policy.workspaceAccess === "none" ? [] : [policy.workspace]),
|
|
40
|
+
input.runtimeDirectory,
|
|
41
|
+
(0, node_path_1.join)(input.memoryRoot, "shared"),
|
|
42
|
+
(0, node_path_1.join)(input.memoryRoot, "projects"),
|
|
43
|
+
ownPrivate,
|
|
44
|
+
];
|
|
45
|
+
// `deny` beats `allow` in Claude Code, and there is no "deny X except Y". Every sibling
|
|
46
|
+
// must therefore be listed explicitly — a role added without regenerating this list
|
|
47
|
+
// leaks, because the missing deny line reads as permission.
|
|
48
|
+
const deny = input.allRoles
|
|
49
|
+
.filter((role) => role !== policy.role)
|
|
50
|
+
.flatMap((role) => {
|
|
51
|
+
const directory = (0, node_path_1.join)(input.memoryRoot, "private", role);
|
|
52
|
+
return [absoluteRule("Read", directory), absoluteRule("Edit", directory)];
|
|
53
|
+
});
|
|
54
|
+
// Tools outside the policy are denied by bare name. Claude Code only honours a path
|
|
55
|
+
// argument on Read and Edit; on the others a bare name is the only rule that applies.
|
|
56
|
+
for (const tool of types_1.TOOL_CATALOG) {
|
|
57
|
+
if (!policy.allowedTools.includes(tool))
|
|
58
|
+
deny.push(tool);
|
|
59
|
+
}
|
|
60
|
+
// The loop above can only deny what ALP itself defines. The runtime's own in-process agent
|
|
61
|
+
// tool is not in `TOOL_CATALOG` and so was never denied at all — the house rule forbidding
|
|
62
|
+
// it was prompt text and nothing more. Named explicitly here so the split rule in
|
|
63
|
+
// `house-rules.ts` is enforced rather than merely stated. No role is granted a subagent
|
|
64
|
+
// today (§4.6); a definition that can declare one is what will take it off this list.
|
|
65
|
+
// `Task` is the tool's older name and ALP never grants it under that spelling. `Agent` is
|
|
66
|
+
// withheld only from a role holding no subagent grant: a bare deny would remove the tool
|
|
67
|
+
// from the model's context, so a role that *is* granted one gets the narrow `Agent(name)`
|
|
68
|
+
// allow rules above instead.
|
|
69
|
+
deny.push("Task");
|
|
70
|
+
if (policy.subagents.length === 0)
|
|
71
|
+
deny.push("Agent");
|
|
72
|
+
// A read-only role keeps that property through two independent mechanisms: no Write/Edit
|
|
73
|
+
// grant, and a sandbox that denies writes to the workspace. Only the second one stops a
|
|
74
|
+
// shell redirect, so where no sandbox exists the shell has to go instead. Losing Bash
|
|
75
|
+
// makes a specialist less capable; losing read-only makes its policy a lie.
|
|
76
|
+
if (policy.workspaceMode === "read-only" && input.sandboxed === false && !deny.includes("Bash")) {
|
|
77
|
+
deny.push("Bash");
|
|
78
|
+
}
|
|
79
|
+
// Defence in depth: a delegated role must never shell out to a raw runtime, whatever
|
|
80
|
+
// its tool grant says. Mirrors `src/policy/invariants.ts`.
|
|
81
|
+
deny.push("Bash(herdr:*)", "Bash(paseo:*)");
|
|
82
|
+
return {
|
|
83
|
+
defaultMode: "default",
|
|
84
|
+
additionalDirectories: Object.freeze(additionalDirectories),
|
|
85
|
+
allow: Object.freeze(allow),
|
|
86
|
+
deny: Object.freeze(deny),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The single TOML basic-string serializer for everything ALP writes into Codex config.
|
|
91
|
+
*
|
|
92
|
+
* `JSON.stringify` is the right primitive rather than a coincidence: TOML basic strings use
|
|
93
|
+
* the same double-quote delimiter and the same `\"`, `\\`, `\n`, `\t`, `\uXXXX` escapes, so
|
|
94
|
+
* the output is valid TOML for any input — including Windows paths full of backslashes and
|
|
95
|
+
* non-ASCII text. It lived in two copies before; keeping one means an escaping bug can only
|
|
96
|
+
* be fixed once.
|
|
97
|
+
*/
|
|
98
|
+
function tomlString(value) {
|
|
99
|
+
return JSON.stringify(value);
|
|
100
|
+
}
|
|
101
|
+
function tomlStringArray(values) {
|
|
102
|
+
return `[${values.map(tomlString).join(", ")}]`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Codex config fragment. Returns TOML lines to append to `codex-config.toml`.
|
|
106
|
+
* `sandbox_mode` is emitted by the adapter itself and is not repeated here.
|
|
107
|
+
*/
|
|
108
|
+
function codexSandboxLines(input) {
|
|
109
|
+
const { policy } = input;
|
|
110
|
+
const writableRoots = policy.workspaceMode === "workspace-write"
|
|
111
|
+
? [policy.workspace, (0, node_path_1.join)(input.memoryRoot, "private", policy.role)]
|
|
112
|
+
: [];
|
|
113
|
+
return Object.freeze([
|
|
114
|
+
// Nothing in a delegated execution should need an approval prompt: the policy already
|
|
115
|
+
// decided what is allowed, and a prompt in a background pane just hangs forever.
|
|
116
|
+
`approval_policy = "never"`,
|
|
117
|
+
...(policy.allowedTools.includes("WebSearch") ? [] : [`web_search = "disabled"`]),
|
|
118
|
+
"",
|
|
119
|
+
"[sandbox_workspace_write]",
|
|
120
|
+
`writable_roots = ${tomlStringArray(writableRoots)}`,
|
|
121
|
+
`network_access = ${policy.allowedTools.includes("WebFetch") || policy.allowedTools.includes("WebSearch")}`,
|
|
122
|
+
"",
|
|
123
|
+
// Command-prefix rules are Codex's equivalent of Claude's `Bash(x:*)` deny entries.
|
|
124
|
+
"[[rules]]",
|
|
125
|
+
`prefix = ["herdr"]`,
|
|
126
|
+
"allow = false",
|
|
127
|
+
"",
|
|
128
|
+
"[[rules]]",
|
|
129
|
+
`prefix = ["paseo"]`,
|
|
130
|
+
"allow = false",
|
|
131
|
+
"",
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Granted MCP servers as `-c` overrides for Codex.
|
|
136
|
+
*
|
|
137
|
+
* They go on argv rather than into `codex-config.toml`, because that file is ALP's own
|
|
138
|
+
* record — Codex loads `$CODEX_HOME/config.toml`, which ALP does not write — and a server
|
|
139
|
+
* declared only there would be documentation of a connection that never happened.
|
|
140
|
+
*
|
|
141
|
+
* Codex has no `--strict-mcp-config`: whatever the principal configured on the machine
|
|
142
|
+
* stays connected alongside these. That asymmetry is real and is stated in §4.6 rather than
|
|
143
|
+
* papered over — the grant adds servers here, where on Claude it also removes them.
|
|
144
|
+
*/
|
|
145
|
+
function codexMcpOverrides(policy) {
|
|
146
|
+
return Object.freeze(policy.mcpServers.flatMap((server) => {
|
|
147
|
+
const fields = [
|
|
148
|
+
`command = ${tomlString(server.command)}`,
|
|
149
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
150
|
+
...(server.env === undefined ? [] : [
|
|
151
|
+
`env = { ${Object.entries(server.env).map(([key, value]) => `${key} = ${tomlString(value)}`).join(", ")} }`,
|
|
152
|
+
]),
|
|
153
|
+
];
|
|
154
|
+
return ["-c", `mcp_servers.${server.name}={ ${fields.join(", ")} }`];
|
|
155
|
+
}));
|
|
156
|
+
}
|