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.
Files changed (204) hide show
  1. package/CHANGELOG.md +770 -0
  2. package/LICENSE +21 -0
  3. package/README.md +295 -0
  4. package/alp.config.yaml +5 -0
  5. package/dist/src/agents/agent-definition.js +28 -0
  6. package/dist/src/agents/capability-catalog.js +33 -0
  7. package/dist/src/agents/compaction.js +36 -0
  8. package/dist/src/agents/errors.js +12 -0
  9. package/dist/src/agents/librarian.js +38 -0
  10. package/dist/src/agents/main.js +37 -0
  11. package/dist/src/agents/memory-grant.js +29 -0
  12. package/dist/src/agents/model-context.js +70 -0
  13. package/dist/src/agents/modes.js +134 -0
  14. package/dist/src/agents/oracle.js +36 -0
  15. package/dist/src/agents/read-thread.js +38 -0
  16. package/dist/src/agents/registry.js +238 -0
  17. package/dist/src/agents/render-identity.js +38 -0
  18. package/dist/src/agents/review.js +37 -0
  19. package/dist/src/agents/search.js +37 -0
  20. package/dist/src/agents/shared/house-rules.js +33 -0
  21. package/dist/src/agents/shared/principal.js +18 -0
  22. package/dist/src/agents/shared/voice.js +29 -0
  23. package/dist/src/agents/titling.js +32 -0
  24. package/dist/src/agents/types.js +15 -0
  25. package/dist/src/backend/execution-backend.js +2 -0
  26. package/dist/src/backend/local-execution-store.js +144 -0
  27. package/dist/src/backend/local-process-backend.js +533 -0
  28. package/dist/src/backend/local-supervisor.js +104 -0
  29. package/dist/src/cli/alp.js +380 -0
  30. package/dist/src/cli/commands/context.js +203 -0
  31. package/dist/src/cli/commands/delegate.js +136 -0
  32. package/dist/src/cli/commands/identity-sync.js +31 -0
  33. package/dist/src/cli/commands/init.js +184 -0
  34. package/dist/src/cli/commands/mode.js +22 -0
  35. package/dist/src/cli/commands/principal.js +114 -0
  36. package/dist/src/cli/commands/run-main.js +90 -0
  37. package/dist/src/cli/commands/runtime.js +21 -0
  38. package/dist/src/cli/mode-preference-store.js +62 -0
  39. package/dist/src/cli/mode-selector.js +178 -0
  40. package/dist/src/cli/update-check.js +77 -0
  41. package/dist/src/context/checkpoint.js +134 -0
  42. package/dist/src/context/compact-journal.js +153 -0
  43. package/dist/src/context/compact-payload.js +121 -0
  44. package/dist/src/context/continuity.js +70 -0
  45. package/dist/src/context/types.js +2 -0
  46. package/dist/src/delegation/backend-registry.js +40 -0
  47. package/dist/src/delegation/delegation-service.js +300 -0
  48. package/dist/src/delegation/types.js +12 -0
  49. package/dist/src/execution/execution-policy.js +96 -0
  50. package/dist/src/execution/execution-service.js +115 -0
  51. package/dist/src/execution/execution-store.js +78 -0
  52. package/dist/src/execution/identity-capsule.js +65 -0
  53. package/dist/src/execution/types.js +12 -0
  54. package/dist/src/hooks/execution-bridge.js +84 -0
  55. package/dist/src/index.js +4 -0
  56. package/dist/src/memory/adapters/markdown-file-store.js +257 -0
  57. package/dist/src/memory/adapters/memory-api-client.js +2 -0
  58. package/dist/src/memory/adapters/memory-path-mapper.js +76 -0
  59. package/dist/src/memory/adapters/remote-api-store.js +25 -0
  60. package/dist/src/memory/context-ranker.js +21 -0
  61. package/dist/src/memory/errors.js +58 -0
  62. package/dist/src/memory/memory-service.js +149 -0
  63. package/dist/src/memory/memory-store.js +2 -0
  64. package/dist/src/memory/types.js +2 -0
  65. package/dist/src/policy/capability-policy.js +29 -0
  66. package/dist/src/policy/delegation-policy.js +25 -0
  67. package/dist/src/policy/errors.js +10 -0
  68. package/dist/src/policy/invariants.js +31 -0
  69. package/dist/src/policy/memory-policy.js +22 -0
  70. package/dist/src/policy/policy-engine.js +85 -0
  71. package/dist/src/policy/types.js +8 -0
  72. package/dist/src/policy/workspace-policy.js +77 -0
  73. package/dist/src/principal/principal-profile-store.js +89 -0
  74. package/dist/src/runtime/adapter-files.js +147 -0
  75. package/dist/src/runtime/claude-adapter.js +177 -0
  76. package/dist/src/runtime/codex-adapter.js +169 -0
  77. package/dist/src/runtime/permission-rules.js +156 -0
  78. package/dist/src/runtime/render-session-context.js +124 -0
  79. package/dist/src/runtime/render-task-input.js +33 -0
  80. package/dist/src/runtime/runtime-adapter.js +2 -0
  81. package/dist/src/runtime/runtime-preference-store.js +66 -0
  82. package/dist/src/runtime/runtime-selector.js +178 -0
  83. package/dist/src/runtime/types.js +2 -0
  84. package/dist/src/runtime/windows-shim.js +57 -0
  85. package/dist/src/state-paths.js +49 -0
  86. package/dist/src/workflow/output-validator.js +27 -0
  87. package/dist/src/workflow/repair-policy.js +8 -0
  88. package/dist/src/workflow/types.js +22 -0
  89. package/dist/src/workflow/workflow-runner.js +81 -0
  90. package/hooks/compact-record.cjs +109 -0
  91. package/hooks/session-boot.cjs +112 -0
  92. package/hooks/session-end.cjs +34 -0
  93. package/package.json +48 -0
  94. package/scaffold/memory/INDEX.md +27 -0
  95. package/scaffold/memory/README.md +76 -0
  96. package/scaffold/memory/projects/INDEX.md +22 -0
  97. package/scaffold/memory/projects/PROTOCOL.md +128 -0
  98. package/scaffold/memory/projects/_template/PROJECT.md +45 -0
  99. package/scripts/alp.cjs +126 -0
  100. package/scripts/alp.ps1 +4 -0
  101. package/scripts/alp.sh +3 -0
  102. package/scripts/bootstrap.cjs +144 -0
  103. package/scripts/checkout-release.cjs +30 -0
  104. package/scripts/delegate.cjs +19 -0
  105. package/scripts/doctor.cjs +158 -0
  106. package/scripts/doctor.sh +3 -0
  107. package/scripts/ensure-state.cjs +22 -0
  108. package/scripts/lib/cli-link.cjs +375 -0
  109. package/scripts/lib/codex-role.cjs +18 -0
  110. package/scripts/lib/delegation/command-runner.cjs +108 -0
  111. package/scripts/lib/delegation/config.cjs +81 -0
  112. package/scripts/lib/install-paths.cjs +154 -0
  113. package/scripts/lib/release-manifest.cjs +42 -0
  114. package/scripts/lib/semver-lite.cjs +20 -0
  115. package/scripts/lib/state.cjs +274 -0
  116. package/scripts/lib/uninstall.cjs +252 -0
  117. package/scripts/lib/update-check-worker.cjs +21 -0
  118. package/scripts/lib/update.cjs +395 -0
  119. package/scripts/run-role.cjs +42 -0
  120. package/scripts/run-role.ps1 +4 -0
  121. package/scripts/run-role.sh +3 -0
  122. package/scripts/sync-project-index.sh +167 -0
  123. package/skills/agent-memory/SKILL.md +109 -0
  124. package/skills/alp-debug/SKILL.md +90 -0
  125. package/skills/alp-debug/references/defense-in-depth.md +118 -0
  126. package/skills/alp-debug/references/investigation-methodology.md +106 -0
  127. package/skills/alp-debug/references/log-and-ci-analysis.md +96 -0
  128. package/skills/alp-debug/references/performance-diagnostics.md +112 -0
  129. package/skills/alp-debug/references/reporting-standards.md +120 -0
  130. package/skills/alp-debug/references/root-cause-tracing.md +134 -0
  131. package/skills/alp-debug/references/systematic-debugging.md +93 -0
  132. package/skills/alp-debug/references/verification.md +86 -0
  133. package/skills/alp-debug/scripts/find-polluter.sh +63 -0
  134. package/skills/alp-debug/scripts/find-polluter.test.md +102 -0
  135. package/skills/alp-plan/SKILL.md +128 -0
  136. package/skills/alp-plan/references/archive-workflow.md +77 -0
  137. package/skills/alp-plan/references/codebase-understanding.md +55 -0
  138. package/skills/alp-plan/references/output-standards.md +96 -0
  139. package/skills/alp-plan/references/plan-organization.md +129 -0
  140. package/skills/alp-plan/references/red-team-personas.md +76 -0
  141. package/skills/alp-plan/references/red-team-workflow.md +81 -0
  142. package/skills/alp-plan/references/research-phase.md +57 -0
  143. package/skills/alp-plan/references/scope-challenge.md +82 -0
  144. package/skills/alp-plan/references/solution-design.md +76 -0
  145. package/skills/alp-plan/references/validate-question-framework.md +89 -0
  146. package/skills/alp-plan/references/validate-workflow.md +83 -0
  147. package/skills/alp-predict/SKILL.md +98 -0
  148. package/skills/alp-scenario/SKILL.md +86 -0
  149. package/skills/code-review/SKILL.md +111 -0
  150. package/skills/code-review/references/code-review-reception.md +114 -0
  151. package/skills/code-review/references/edge-case-scouting.md +78 -0
  152. package/skills/code-review/references/verification-before-completion.md +117 -0
  153. package/skills/delegation/SKILL.md +46 -0
  154. package/skills/docs-seeker/.env.example +15 -0
  155. package/skills/docs-seeker/SKILL.md +87 -0
  156. package/skills/docs-seeker/package.json +25 -0
  157. package/skills/docs-seeker/references/advanced.md +82 -0
  158. package/skills/docs-seeker/references/context7-patterns.md +68 -0
  159. package/skills/docs-seeker/references/errors.md +72 -0
  160. package/skills/docs-seeker/scripts/analyze-llms-txt.js +211 -0
  161. package/skills/docs-seeker/scripts/detect-topic.js +172 -0
  162. package/skills/docs-seeker/scripts/fetch-docs.js +213 -0
  163. package/skills/docs-seeker/scripts/tests/run-tests.js +72 -0
  164. package/skills/docs-seeker/scripts/tests/test-analyze-llms.js +119 -0
  165. package/skills/docs-seeker/scripts/tests/test-detect-topic.js +112 -0
  166. package/skills/docs-seeker/scripts/tests/test-fetch-docs.js +84 -0
  167. package/skills/docs-seeker/scripts/utils/env-loader.js +94 -0
  168. package/skills/docs-seeker/workflows/library-search.md +73 -0
  169. package/skills/docs-seeker/workflows/repo-analysis.md +90 -0
  170. package/skills/docs-seeker/workflows/topic-search.md +69 -0
  171. package/skills/git/SKILL.md +121 -0
  172. package/skills/git/references/branch-management.md +90 -0
  173. package/skills/git/references/commit-standards.md +82 -0
  174. package/skills/git/references/gh-cli-guide.md +132 -0
  175. package/skills/git/references/safety-protocols.md +86 -0
  176. package/skills/git/references/workflow-commit.md +89 -0
  177. package/skills/git/references/workflow-merge.md +63 -0
  178. package/skills/git/references/workflow-pr.md +70 -0
  179. package/skills/git/references/workflow-push.md +62 -0
  180. package/skills/gkg/SKILL.md +87 -0
  181. package/skills/gkg/references/cli-commands.md +92 -0
  182. package/skills/gkg/references/http-api.md +99 -0
  183. package/skills/gkg/references/language-support.md +54 -0
  184. package/skills/problem-solving/SKILL.md +86 -0
  185. package/skills/problem-solving/references/attribution.md +48 -0
  186. package/skills/problem-solving/references/collision-zone-thinking.md +71 -0
  187. package/skills/problem-solving/references/inversion-exercise.md +88 -0
  188. package/skills/problem-solving/references/meta-pattern-recognition.md +80 -0
  189. package/skills/problem-solving/references/scale-game.md +82 -0
  190. package/skills/problem-solving/references/simplification-cascades.md +83 -0
  191. package/skills/problem-solving/references/when-stuck.md +76 -0
  192. package/skills/repomix/SKILL.md +94 -0
  193. package/skills/repomix/references/configuration.md +134 -0
  194. package/skills/repomix/references/usage-patterns.md +106 -0
  195. package/skills/repomix/scripts/.coverage +0 -0
  196. package/skills/repomix/scripts/README.md +179 -0
  197. package/skills/repomix/scripts/repomix_batch.py +455 -0
  198. package/skills/repomix/scripts/repos.example.json +15 -0
  199. package/skills/repomix/scripts/requirements.txt +15 -0
  200. package/skills/repomix/scripts/tests/test_repomix_batch.py +531 -0
  201. package/skills/research/SKILL.md +107 -0
  202. package/skills/security-scan/SKILL.md +101 -0
  203. package/skills/security-scan/references/secret-patterns.md +75 -0
  204. 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
+ }