@aiwg/cli 2026.10.5 → 2026.10.7

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 (263) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +7 -3
  6. package/dist/src/a2a/agent-card.js +11 -0
  7. package/dist/src/api/index.d.ts +1 -0
  8. package/dist/src/api/index.js +1 -0
  9. package/dist/src/artifacts/browser-export.js +1 -0
  10. package/dist/src/artifacts/citation-parser.js +8 -1
  11. package/dist/src/artifacts/cli.js +57 -6
  12. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  13. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  14. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  15. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  16. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  17. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  18. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  19. package/dist/src/artifacts/graph-query.js +18 -8
  20. package/dist/src/artifacts/index-adapters.js +339 -0
  21. package/dist/src/artifacts/index-builder.js +143 -14
  22. package/dist/src/artifacts/index-files.js +15 -4
  23. package/dist/src/artifacts/index-preparation.js +2 -0
  24. package/dist/src/artifacts/index-reader.js +44 -0
  25. package/dist/src/artifacts/index-refresh.js +114 -55
  26. package/dist/src/artifacts/index-status.js +20 -5
  27. package/dist/src/artifacts/memory-maintenance.js +503 -0
  28. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  29. package/dist/src/artifacts/query-engine.js +263 -108
  30. package/dist/src/artifacts/relations-builder.js +2 -2
  31. package/dist/src/artifacts/source-graph.js +2 -2
  32. package/dist/src/artifacts/types.js +9 -1
  33. package/dist/src/auth/client.js +5 -1
  34. package/dist/src/auth/credential-store.js +5 -1
  35. package/dist/src/auth/sensitive-input.js +151 -0
  36. package/dist/src/channel/manager.mjs +3 -0
  37. package/dist/src/cli/handlers/agentcard.js +47 -25
  38. package/dist/src/cli/handlers/auth.js +22 -2
  39. package/dist/src/cli/handlers/decision.js +22 -3
  40. package/dist/src/cli/handlers/effect.js +4 -84
  41. package/dist/src/cli/handlers/guided-setup.js +470 -0
  42. package/dist/src/cli/handlers/help.js +2 -2
  43. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  44. package/dist/src/cli/handlers/index.js +4 -2
  45. package/dist/src/cli/handlers/init.js +41 -6
  46. package/dist/src/cli/handlers/install.js +6 -0
  47. package/dist/src/cli/handlers/local-executor.js +5 -3
  48. package/dist/src/cli/handlers/marketplace.js +15 -4
  49. package/dist/src/cli/handlers/mc.js +509 -53
  50. package/dist/src/cli/handlers/models.js +32 -2
  51. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  52. package/dist/src/cli/handlers/rules.js +1 -1
  53. package/dist/src/cli/handlers/script-runner.js +73 -2
  54. package/dist/src/cli/handlers/serve.js +5 -3
  55. package/dist/src/cli/handlers/sessions.js +1 -0
  56. package/dist/src/cli/handlers/setup.js +432 -1
  57. package/dist/src/cli/handlers/skill-lint.js +1 -1
  58. package/dist/src/cli/handlers/subcommands.js +118 -8
  59. package/dist/src/cli/handlers/use.js +335 -107
  60. package/dist/src/cli/handlers/workspace.js +3 -1
  61. package/dist/src/cli/prompt-utils.js +81 -0
  62. package/dist/src/cli/router.js +16 -1
  63. package/dist/src/config/aiwg-config.js +42 -0
  64. package/dist/src/config/cli.js +7 -3
  65. package/dist/src/config/decision-config.js +14 -1
  66. package/dist/src/config/gitignore.js +4 -0
  67. package/dist/src/config/native-sandbox-warnings.js +25 -0
  68. package/dist/src/config/workspace.js +2 -1
  69. package/dist/src/contributors/validation.js +6 -5
  70. package/dist/src/decision/adapters/jev.js +20 -31
  71. package/dist/src/decision/admission.js +7 -3
  72. package/dist/src/decision/ask.js +6 -1
  73. package/dist/src/decision/benchmark.js +482 -0
  74. package/dist/src/decision/ledger.js +22 -3
  75. package/dist/src/decision/pricing.js +147 -0
  76. package/dist/src/decision/qualification/gate-evidence.js +23 -18
  77. package/dist/src/decision/qualification/load-staged.js +576 -0
  78. package/dist/src/decision/qualification/load.js +109 -33
  79. package/dist/src/decision/qualification/runner.js +12 -5
  80. package/dist/src/decision/review/service.js +6 -4
  81. package/dist/src/decision/review/validate.js +23 -2
  82. package/dist/src/decision/savings.js +8 -3
  83. package/dist/src/discovery/index.js +492 -0
  84. package/dist/src/discovery/interview.js +238 -0
  85. package/dist/src/discovery/plan-apply.js +567 -0
  86. package/dist/src/discovery/plan-store.js +171 -0
  87. package/dist/src/discovery/plan-types.js +2 -0
  88. package/dist/src/discovery/plan-validation.js +139 -0
  89. package/dist/src/discovery/types.js +2 -0
  90. package/dist/src/effects/host.js +91 -0
  91. package/dist/src/extensions/commands/definitions.js +49 -9
  92. package/dist/src/extensions/manifest.js +71 -44
  93. package/dist/src/extensions/project-local-discovery.js +1 -1
  94. package/dist/src/extensions/project-local-gitignore.js +2 -0
  95. package/dist/src/extensions/project-quickref.js +18 -17
  96. package/dist/src/extensions/validation.js +32 -29
  97. package/dist/src/housekeeper/daemon-config.js +148 -0
  98. package/dist/src/housekeeper/ipc.js +84 -0
  99. package/dist/src/housekeeper/lifecycle.js +148 -0
  100. package/dist/src/housekeeper/platform.js +35 -0
  101. package/dist/src/housekeeper/status.js +70 -0
  102. package/dist/src/housekeeper/units.js +114 -0
  103. package/dist/src/issues/cli.js +16 -5
  104. package/dist/src/issues/live.js +44 -12
  105. package/dist/src/jobs/flow.js +22 -21
  106. package/dist/src/lint/rule-activation.js +20 -5
  107. package/dist/src/lint/runner.js +1 -1
  108. package/dist/src/marketplace/exchange.js +18 -6
  109. package/dist/src/marketplace/provenance.js +3 -2
  110. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  111. package/dist/src/mcp/cli.mjs +22 -96
  112. package/dist/src/mcp/config-file.mjs +6 -2
  113. package/dist/src/mcp/config-layers.mjs +23 -7
  114. package/dist/src/mcp/grok-build-config.mjs +2 -1
  115. package/dist/src/mcp/muse-native-config.mjs +2 -1
  116. package/dist/src/mcp/profiles.js +18 -31
  117. package/dist/src/mcp/profiles.mjs +18 -32
  118. package/dist/src/mcp/registry.js +7 -27
  119. package/dist/src/mcp/registry.mjs +7 -28
  120. package/dist/src/mcp/server.mjs +34 -1
  121. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  122. package/dist/src/mcp/tools/command-run.mjs +3 -2
  123. package/dist/src/mcp/tools/decision.mjs +49 -48
  124. package/dist/src/mcp/tools/discovery.mjs +46 -21
  125. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  126. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  127. package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
  128. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  129. package/dist/src/memory/audit.js +719 -0
  130. package/dist/src/memory/canonical-context.js +392 -13
  131. package/dist/src/memory/canonical-maintenance.js +95 -0
  132. package/dist/src/memory/cli-result.js +22 -0
  133. package/dist/src/memory/cli.js +176 -0
  134. package/dist/src/memory/context-pack.js +57 -6
  135. package/dist/src/memory/cost.js +512 -0
  136. package/dist/src/memory/firewall-classification.d.mts +7 -0
  137. package/dist/src/memory/firewall-classification.mjs +32 -0
  138. package/dist/src/memory/index.js +8 -0
  139. package/dist/src/memory/line-maintenance.js +68 -0
  140. package/dist/src/memory/maintenance-cli.js +286 -0
  141. package/dist/src/memory/maintenance-contract.js +136 -0
  142. package/dist/src/memory/maintenance-index.js +18 -0
  143. package/dist/src/memory/maintenance-owner.js +2 -0
  144. package/dist/src/memory/maintenance-record.js +138 -0
  145. package/dist/src/memory/maintenance.js +254 -0
  146. package/dist/src/memory/project-registry.js +2 -1
  147. package/dist/src/memory/provider-memory-adapter.js +154 -0
  148. package/dist/src/memory/recommend-cli.js +83 -0
  149. package/dist/src/memory/recommend.js +269 -0
  150. package/dist/src/memory/scheduled.js +81 -0
  151. package/dist/src/memory/taxonomy.js +44 -0
  152. package/dist/src/mission-protocol/codecs.js +19 -9
  153. package/dist/src/mission-protocol/worker-status.js +91 -1
  154. package/dist/src/models/config-loader.js +11 -1
  155. package/dist/src/models/model-capabilities.v1.json +74 -50
  156. package/dist/src/models/provider-policy.js +125 -62
  157. package/dist/src/models/resolver.js +45 -8
  158. package/dist/src/models/worker-installation.d.mts +8 -1
  159. package/dist/src/models/worker-installation.mjs +231 -38
  160. package/dist/src/models/wrapper-route.js +1 -1
  161. package/dist/src/output-modes/registry.js +8 -7
  162. package/dist/src/packages/adapters/git.js +99 -23
  163. package/dist/src/packages/adapters/gitea.js +8 -27
  164. package/dist/src/packages/adapters/github.js +7 -25
  165. package/dist/src/packages/registry.js +4 -4
  166. package/dist/src/plugin/metadata-validator.js +1 -1
  167. package/dist/src/process/cli-command.d.mts +5 -0
  168. package/dist/src/process/cli-command.mjs +42 -0
  169. package/dist/src/providers/deploy-version.d.mts +1 -0
  170. package/dist/src/providers/deploy-version.mjs +17 -0
  171. package/dist/src/providers/provider-definitions.js +19 -16
  172. package/dist/src/providers/skill-ownership.d.mts +11 -2
  173. package/dist/src/providers/skill-ownership.mjs +35 -17
  174. package/dist/src/rules/activation.d.mts +11 -2
  175. package/dist/src/rules/activation.mjs +82 -6
  176. package/dist/src/rules/inventory.js +1 -1
  177. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  178. package/dist/src/schema/zod-validation.d.mts +6 -0
  179. package/dist/src/schema/zod-validation.mjs +50 -0
  180. package/dist/src/serve/executor-registry.js +297 -20
  181. package/dist/src/serve/executor-token.d.mts +1 -0
  182. package/dist/src/serve/executor-token.mjs +8 -0
  183. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  184. package/dist/src/serve/worker-invocation.js +124 -15
  185. package/dist/src/sessions/adapters/claude.js +19 -18
  186. package/dist/src/sessions/adapters/codex.js +10 -9
  187. package/dist/src/sessions/adapters/copilot.js +14 -13
  188. package/dist/src/sessions/adapters/cursor.js +19 -18
  189. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  190. package/dist/src/sessions/adapters/factory.js +11 -10
  191. package/dist/src/sessions/adapters/generic.js +32 -23
  192. package/dist/src/sessions/adapters/grokbot.js +35 -35
  193. package/dist/src/sessions/adapters/hermes.js +20 -19
  194. package/dist/src/sessions/adapters/muse.js +50 -49
  195. package/dist/src/sessions/adapters/omp.js +5 -4
  196. package/dist/src/sessions/adapters/openclaw.js +30 -29
  197. package/dist/src/sessions/adapters/opencode.js +26 -26
  198. package/dist/src/sessions/adapters/openhuman.js +24 -23
  199. package/dist/src/sessions/adapters/pi.js +5 -4
  200. package/dist/src/sessions/adapters/windsurf.js +8 -8
  201. package/dist/src/sessions/analytics.js +10 -9
  202. package/dist/src/sessions/candidates.js +7 -6
  203. package/dist/src/sessions/contracts.js +63 -53
  204. package/dist/src/sessions/output-registration.js +11 -10
  205. package/dist/src/sessions/workspace-discovery.js +3 -4
  206. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  207. package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
  208. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  209. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  210. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  211. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  212. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  213. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  214. package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
  215. package/dist/src/tracker/project-inputs.js +2 -0
  216. package/dist/src/update/checker.d.mts +4 -0
  217. package/dist/src/update/checker.mjs +14 -0
  218. package/dist/src/update/notifier.mjs +2 -4
  219. package/dist/src/writing/exemplar-selection.js +6 -6
  220. package/dist/src/writing/voice-evaluation.js +24 -23
  221. package/dist/src/writing/writer-migration.js +14 -13
  222. package/dist/src/writing/writer-profile.js +19 -18
  223. package/dist/src/writing/writing-brief.js +18 -17
  224. package/dist/src/writing/writing-receipt.js +47 -46
  225. package/package.json +5 -2
  226. package/schemas/decision/DecisionReview.schema.json +78 -1
  227. package/schemas/mission-v1.schema.json +3 -0
  228. package/tools/agents/deploy-agents.mjs +67 -39
  229. package/tools/agents/providers/antigravity.mjs +6 -1
  230. package/tools/agents/providers/base.mjs +24 -14
  231. package/tools/agents/providers/claude.mjs +6 -3
  232. package/tools/agents/providers/codex.mjs +54 -57
  233. package/tools/agents/providers/context-write.mjs +28 -0
  234. package/tools/agents/providers/copilot.mjs +13 -4
  235. package/tools/agents/providers/cursor.mjs +1 -1
  236. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  237. package/tools/agents/providers/factory.mjs +7 -1
  238. package/tools/agents/providers/grok-build.mjs +18 -2
  239. package/tools/agents/providers/grokbot.mjs +1 -1
  240. package/tools/agents/providers/hermes.mjs +12 -15
  241. package/tools/agents/providers/muse-hooks.mjs +21 -14
  242. package/tools/agents/providers/muse.mjs +7 -2
  243. package/tools/agents/providers/omp.mjs +31 -5
  244. package/tools/agents/providers/opencode.mjs +14 -5
  245. package/tools/agents/providers/openhuman.mjs +1 -1
  246. package/tools/agents/providers/pi.mjs +1 -1
  247. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  248. package/tools/agents/providers/rule-delivery.mjs +289 -80
  249. package/tools/agents/providers/warp.mjs +4 -1
  250. package/tools/agents/providers/windsurf.mjs +13 -3
  251. package/tools/cli/workspace-status.mjs +5 -0
  252. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  253. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  254. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  255. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  256. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  257. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  258. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  259. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  260. package/tools/ralph-external/process-monitor.mjs +49 -8
  261. package/tools/ralph-external/session-launcher.mjs +405 -35
  262. package/tools/skills/deploy-skills-codex.mjs +11 -7
  263. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -1,3 +1,4 @@
1
+ import { importImpl } from '../../_resolve-impl.mjs';
1
2
  /**
2
3
  * Warp Terminal Provider
3
4
  *
@@ -48,6 +49,8 @@ import {
48
49
  collectFrameworkArtifacts
49
50
  } from './base.mjs';
50
51
 
52
+ import { writeContextFileSync } from './context-write.mjs';
53
+
51
54
  // ============================================================================
52
55
  // Provider Configuration
53
56
  // ============================================================================
@@ -206,7 +209,7 @@ export async function deployWarp(targetDir, srcRoot, opts) {
206
209
  // Preserve operator sections even for a forced artifact refresh.
207
210
  const output = mergeWarpMd(existing, content, false);
208
211
  ensureDir(targetDir, opts.dryRun);
209
- if (!opts.dryRun) fs.writeFileSync(dest, output);
212
+ if (!opts.dryRun) writeContextFileSync(dest, output);
210
213
 
211
214
  }
212
215
 
@@ -1,3 +1,4 @@
1
+ import { importImpl } from '../../_resolve-impl.mjs';
1
2
  /**
2
3
  * Devin Desktop Provider (Windsurf compatibility adapter)
3
4
  *
@@ -30,6 +31,7 @@ let fs;
30
31
  try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
31
32
  import path from 'path';
32
33
  import {
34
+ withRuleDeployment,
33
35
  ensureDir,
34
36
  listMdFiles,
35
37
  listMdFilesRecursive,
@@ -54,6 +56,8 @@ import {
54
56
  deploySoulCompanions
55
57
  } from './base.mjs';
56
58
 
59
+ import { writeContextFileSync } from './context-write.mjs';
60
+
57
61
  // ============================================================================
58
62
  // Provider Configuration
59
63
  // ============================================================================
@@ -234,6 +238,7 @@ export function mapModel(shorthand, modelCfg, modelsConfig) {
234
238
  * Generate AGENTS.md for Windsurf with all agents aggregated
235
239
  */
236
240
  export function generateAgentsMd(files, destPath, opts) {
241
+ if (opts.contextFiles === false) return;
237
242
  const { dryRun } = opts;
238
243
 
239
244
  const lines = [];
@@ -283,7 +288,7 @@ export function generateAgentsMd(files, destPath, opts) {
283
288
  if (dryRun) {
284
289
  console.log(`[dry-run] Would write AGENTS.md with ${agents.length} agents`);
285
290
  } else {
286
- fs.writeFileSync(destPath, output, 'utf8');
291
+ writeContextFileSync(destPath, output, 'utf8');
287
292
  console.log(`Created AGENTS.md with ${agents.length} agents at ${path.relative(process.cwd(), destPath)}`);
288
293
  }
289
294
 
@@ -407,7 +412,7 @@ export function generateWindsurfRules(srcRoot, target, opts) {
407
412
  console.log(`[dry-run] Would write .windsurf/rules/aiwg-orchestration.md (trigger: always_on)`);
408
413
  } else {
409
414
  ensureDir(rulesDir);
410
- fs.writeFileSync(orchestrationPath, output, 'utf8');
415
+ writeContextFileSync(orchestrationPath, output, 'utf8');
411
416
  console.log(`Created .windsurf/rules/aiwg-orchestration.md (trigger: always_on)`);
412
417
  }
413
418
 
@@ -428,7 +433,7 @@ export function generateWindsurfRules(srcRoot, target, opts) {
428
433
  if (dryRun) {
429
434
  console.log(`[dry-run] Would write .windsurfrules (deprecated stub)`);
430
435
  } else {
431
- fs.writeFileSync(stubPath, stubLines.join('\n'), 'utf8');
436
+ writeContextFileSync(stubPath, stubLines.join('\n'), 'utf8');
432
437
  console.log(`Created .windsurfrules (deprecated stub → .windsurf/rules/aiwg-orchestration.md)`);
433
438
  }
434
439
  }
@@ -507,6 +512,7 @@ export function deploySkills(skillDirs, targetDir, opts) {
507
512
  * Deploy rules as discrete files with trigger: always_on frontmatter injected
508
513
  */
509
514
  export function deployRules(ruleFiles, targetDir, opts) {
515
+ if (opts.ruleDelivery) return [];
510
516
  const destDir = path.join(targetDir, paths.rules);
511
517
  ensureDir(destDir, opts.dryRun);
512
518
  cleanupOldRuleFiles(destDir, opts);
@@ -534,6 +540,10 @@ export function getFileExtension(type) {
534
540
  // ============================================================================
535
541
 
536
542
  export async function deploy(opts) {
543
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
544
+ }
545
+
546
+ async function deploySelected(opts) {
537
547
  const {
538
548
  srcRoot,
539
549
  target,
@@ -32,6 +32,7 @@ const __filename = fileURLToPath(import.meta.url);
32
32
  const __dirname = path.dirname(__filename);
33
33
  const packageRoot = path.resolve(__dirname, '../..');
34
34
  const runtimeRoot = existsSync(path.join(packageRoot, 'src')) ? 'src' : 'dist/src';
35
+ const { readNativeSandboxWarnings } = await importImpl(import.meta.url, 'config/native-sandbox-warnings.js');
35
36
  const [{ resolveProjectAiwgDir }, { auditProjectArtifactHealth }] = await Promise.all([
36
37
  import(pathToFileURL(path.join(packageRoot, runtimeRoot, 'config/project-artifacts-runtime.mjs')).href),
37
38
  import(pathToFileURL(path.join(packageRoot, runtimeRoot, 'config/project-artifacts-health.mjs')).href),
@@ -356,6 +357,7 @@ async function buildWorkspaceStatus(projectRoot) {
356
357
  const { rulePinState } = await importImpl(import.meta.url, 'rules/pin-state.js');
357
358
  const pins = rulePinState(projectRoot, packageRoot);
358
359
  const result = {
360
+ nativeSandboxWarnings: await readNativeSandboxWarnings(projectRoot).catch(() => []),
359
361
  rule_pins: { rules: pins.rules, warnings: pins.warnings },
360
362
  decisionOffload: await collectDecisionOffloadReport(projectRoot),
361
363
  workspace: {
@@ -545,6 +547,7 @@ async function buildVerificationProbe(projectRoot) {
545
547
 
546
548
  return {
547
549
  schema: 'aiwg.status.probe.v1',
550
+ nativeSandboxWarnings: workspace.nativeSandboxWarnings,
548
551
  rule_pins: workspace.rule_pins,
549
552
  generated_at: new Date().toISOString(),
550
553
  project_root: path.resolve(projectRoot),
@@ -725,6 +728,7 @@ async function workspaceStatus(args) {
725
728
  console.log('='.repeat(60));
726
729
  console.log('');
727
730
  console.log('No .aiwg/ workspace found in this project.');
731
+ for (const warning of result.nativeSandboxWarnings) console.log(`Warning: ${warning.message}`);
728
732
  console.log('');
729
733
  console.log('To create a workspace:');
730
734
  console.log(' aiwg use sdlc # Install SDLC framework');
@@ -741,6 +745,7 @@ async function workspaceStatus(args) {
741
745
  }
742
746
 
743
747
  // Text output
748
+ for (const warning of result.nativeSandboxWarnings) console.log(`Warning: ${warning.message}`);
744
749
  console.log('\nAIWG - Workspace Status');
745
750
  console.log('='.repeat(60));
746
751
  console.log('');
@@ -27,16 +27,14 @@ const _require = createRequire(import.meta.url);
27
27
  let fs;
28
28
  try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
29
29
  import path from 'path';
30
+ import { fileURLToPath } from 'node:url';
30
31
  import os from 'os';
31
32
  import { getFrameworksForMode, normalizeDeploymentMode, getAddonSkillDirs, listSkillDirs, collectFrameworkArtifacts } from '../agents/providers/base.mjs';
32
33
 
33
- const CODEX_PROMPTS_DIR = path.join(os.homedir(), '.codex', 'prompts');
34
-
35
- function parseArgs() {
36
- const args = process.argv.slice(2);
34
+ function parseArgs(args, homeDir) {
37
35
  const cfg = {
38
36
  source: null,
39
- target: CODEX_PROMPTS_DIR,
37
+ target: path.join(homeDir, '.codex', 'prompts'),
40
38
  mode: 'all',
41
39
  dryRun: false,
42
40
  force: false,
@@ -265,8 +263,8 @@ function getCommandDirectories(srcRoot, mode) {
265
263
  return dirs;
266
264
  }
267
265
 
268
- (async function main() {
269
- const cfg = parseArgs();
266
+ export async function main(args = process.argv.slice(2), internal = {}) {
267
+ const cfg = parseArgs(args, internal.homeDir || os.homedir());
270
268
  const { source, target, mode, dryRun, force, prefix } = cfg;
271
269
 
272
270
  // Resolve source directory
@@ -333,4 +331,9 @@ function getCommandDirectories(srcRoot, mode) {
333
331
  console.log(`\nRestart Codex to load new prompts.`);
334
332
  console.log(`Use prompts with: /prompts:${prefix}-<command-name>`);
335
333
  }
336
- })();
334
+ }
335
+
336
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
337
+ try { await main(); }
338
+ catch (error) { console.error(error.message); process.exitCode = 1; }
339
+ }
@@ -75,7 +75,7 @@ const MODEL_MAP = {
75
75
  */
76
76
  export const CODEX_SUPPORTED_VERSIONS = Object.freeze(['0.161.0']);
77
77
  export const CODEX_DIRECT_EFFORT_VALUES = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh']);
78
- export const CODEX_DIRECT_SANDBOX_MODES = Object.freeze(['read-only', 'workspace-write']);
78
+ export const CODEX_DIRECT_SANDBOX_MODES = Object.freeze(['read-only', 'workspace-write', 'external']);
79
79
  /** Non-prompting execution only: a requested `on-request` is refused. */
80
80
  export const CODEX_DIRECT_APPROVAL_POLICIES = Object.freeze(['never']);
81
81
  /** Brief transport for the direct profile: piped stdin, never argv. */
@@ -195,6 +195,10 @@ export class CodexAdapter extends ProviderAdapter {
195
195
  };
196
196
  }
197
197
 
198
+ // Worker envNames come from the recorded installation environment, filtered
199
+ // through both registry allowlists. PATH must survive env -i so Codex can
200
+ // find the host's AppArmor-approved bwrap and system shell tools.
201
+
198
202
  /**
199
203
  * Driver-owned argv after the binary for the version-bound direct-shell
200
204
  * profile. Every control is explicit: `--ignore-user-config` (user
@@ -252,7 +256,7 @@ export class CodexAdapter extends ProviderAdapter {
252
256
  '--ignore-user-config',
253
257
  '-m', model,
254
258
  '-c', `model_reasoning_effort=${JSON.stringify(effort)}`,
255
- '-s', sandbox,
259
+ '-s', sandbox === 'external' ? 'danger-full-access' : sandbox,
256
260
  '-C', cwd,
257
261
  '-c', `approval_policy=${JSON.stringify(approval)}`,
258
262
  '--skip-git-repo-check',
@@ -308,6 +308,9 @@ const HOST_CODES = new Set(['RESOURCE', 'NOT_FOUND', 'SANDBOX']);
308
308
  const PROVIDER_CODES = new Set(['NETWORK']);
309
309
  const INSTALLATION_CODES = new Set(['AUTH']);
310
310
  const MODEL_CODES = new Set(['MODEL']);
311
+ const STATUS_REASON_CODES = new Set(['NOT_FOUND', 'SPAWN_REFUSED', 'RATE_LIMIT', 'RESOURCE', 'SANDBOX',
312
+ 'NETWORK', 'AUTH', 'MODEL', 'UNKNOWN']);
313
+ export const RECOVERY_STATUS_LIMIT = 32;
311
314
 
312
315
  export const scopeKeyFor = Object.freeze({
313
316
  invocation: (id) => `invocation:${id ?? 'unknown'}`,
@@ -511,6 +514,7 @@ export class ScopedFailureBreaker extends EventEmitter {
511
514
  this._scopes = new Map();
512
515
  this._outstanding = new Map();
513
516
  this._hostFailureFacts = [];
517
+ this._transitions = [];
514
518
  this._canarySeq = 0;
515
519
  this._observedInvocations = new Set();
516
520
  this.canariesObserved = 0;
@@ -1022,6 +1026,10 @@ export class ScopedFailureBreaker extends EventEmitter {
1022
1026
  const from = rec.status;
1023
1027
  if (from === to) return;
1024
1028
  rec.status = to;
1029
+ if (rec.scopeKey.length <= 256) this._transitions.push({ scopeKey: rec.scopeKey, scope: rec.scope, from, to,
1030
+ reasons: [...rec.evidence.keys()].filter(code => STATUS_REASON_CODES.has(code)).slice(0, 8),
1031
+ atMs: this.nowMs(), nextEligibleAtMs: to === 'closed' ? null : rec.nextProbeAtMs });
1032
+ if (this._transitions.length > RECOVERY_STATUS_LIMIT) this._transitions.shift();
1025
1033
  this.emit('transition', { scopeKey: rec.scopeKey, from, to, action,
1026
1034
  distinctFailures: new Set(rec.failures.map(failure => failure.invocationId)).size, reasons: [...rec.evidence.keys()],
1027
1035
  nextEligibleAtMs: rec.nextProbeAtMs, resetAtMs: rec.resetAtMs,
@@ -1454,6 +1462,23 @@ export class ScopedFailureBreaker extends EventEmitter {
1454
1462
  };
1455
1463
  }
1456
1464
 
1465
+ /** Pure status projection: never sweep reservations, probe, or advance state. */
1466
+ recoveryStatus() {
1467
+ const recentKeys = [...new Set(this._transitions.map(event => event.scopeKey).reverse())];
1468
+ const active = [...this._scopes.values()].filter(rec => rec.status !== 'closed');
1469
+ const keys = [...new Set([...active.map(rec => rec.scopeKey), ...recentKeys])];
1470
+ const scopes = keys.filter(key => key.length <= 256).slice(0, RECOVERY_STATUS_LIMIT).map(scopeKey => {
1471
+ const rec = this._scopes.get(scopeKey);
1472
+ const last = this._transitions.findLast(event => event.scopeKey === scopeKey);
1473
+ return { scopeKey, scope: rec?.scope ?? last.scope, state: rec?.status ?? last.to,
1474
+ reasons: rec ? [...rec.evidence.keys()].filter(code => STATUS_REASON_CODES.has(code)).slice(0, 8) : [...last.reasons],
1475
+ nextEligibleAtMs: !rec || rec.status === 'closed' ? null : rec.nextProbeAtMs,
1476
+ canaryInFlight: rec?.activeCanary != null };
1477
+ });
1478
+ return { observedAtMs: this.nowMs(), scopes, omittedScopeCount: Math.max(0, keys.length - scopes.length),
1479
+ transitions: this._transitions.map(event => ({ ...event, reasons: [...event.reasons] })) };
1480
+ }
1481
+
1457
1482
  /** Correlate launcher-observed host faults across resolved inference backends.
1458
1483
  * Replay the first fault once corroborated, preserving the three-distinct
1459
1484
  * threshold even when the first backend alone could not establish scope.
@@ -1550,6 +1575,7 @@ export class ScopedFailureBreaker extends EventEmitter {
1550
1575
 
1551
1576
  destroy() {
1552
1577
  this._hostFailureFacts = [];
1578
+ this._transitions = [];
1553
1579
  for (const rec of this._scopes.values()) rec.breaker.destroy();
1554
1580
  this._scopes.clear();
1555
1581
  this._outstanding.clear();
@@ -3,6 +3,7 @@ import { runRecoveryProbe } from './recovery-probe.mjs';
3
3
  /** Recovery evidence observed outside worker output by the launch owner. */
4
4
  const observations = new WeakMap();
5
5
  const failureHints = new WeakMap();
6
+ const assessed = new WeakSet();
6
7
 
7
8
  export function recordLauncherRecoveryHint(result, reasonCode) {
8
9
  failureHints.set(result, reasonCode);
@@ -23,9 +24,10 @@ export function launcherRecoveryEvidence(result) {
23
24
  */
24
25
  export async function observeLauncherRecoveryOutcome({ invocation, result, installation, breaker,
25
26
  failureHint = '', beforeRequest, onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now, timeoutMs = 10_000 } = {}) {
26
- if (!result || result.attemptOutcome !== 'failed' || result.cancelled) return;
27
+ if (!result || result.attemptOutcome !== 'failed' || result.cancelled || assessed.has(result)) return;
27
28
  const backend = invocation?.selected?.inferenceProvider;
28
29
  if (HOST_ERRORS.has(result.errorCode) || HOST_RESULTS.has(result.nativeResult)) {
30
+ assessed.add(result);
29
31
  const corroboratedBackends = breaker?.observeHostFailure({ invocationId: result.invocationId,
30
32
  inferenceProvider: backend, reasonCode: 'RESOURCE' }) ?? [];
31
33
  if (corroboratedBackends.length >= 2) observations.set(result, {
@@ -43,6 +45,10 @@ export async function observeLauncherRecoveryOutcome({ invocation, result, insta
43
45
  if (!await beforeRequest()) return;
44
46
  const requestTimeoutMs = remaining();
45
47
  if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) return;
48
+ // One diagnostic request per attempt, including unsuccessful diagnostics.
49
+ // Re-reading a result cannot consume allowance or create fresh evidence.
50
+ if (assessed.has(result)) return;
51
+ assessed.add(result);
46
52
  const observed = await runRecoveryProbe({ driver: { qualification: { installation } }, invocation: invocation ?? { transport: 'shell' },
47
53
  timeoutMs: Math.min(10_000, requestTimeoutMs), maxOutputBytes: 65_536, onDispatch, fetchImpl, wallNowMs });
48
54
  if (observed.reason === 'probe-rate-limited') {
@@ -162,12 +162,15 @@ export class MuseAdapter extends ProviderAdapter {
162
162
  }
163
163
  let schema;
164
164
  try { schema = JSON.parse(profile.controls.structuredOutput.mapping); } catch { /* unavailable mapping blocks */ }
165
- if (!Array.isArray(schema) || schema.length !== 2 || schema[0] !== '--output-schema'
166
- || typeof schema[1] !== 'string' || !path.isAbsolute(schema[1]) || schema[1].includes('\0') || schema[1].split(path.sep).includes('..')) {
165
+ const directText = profile.controls.structuredOutput.mapping === '[]'
166
+ && profile.controls.structuredOutput.support === 'unqualified';
167
+ if (!directText && (!Array.isArray(schema) || schema.length !== 2 || schema[0] !== '--output-schema'
168
+ || typeof schema[1] !== 'string' || !path.isAbsolute(schema[1]) || schema[1].includes('\0') || schema[1].split(path.sep).includes('..'))) {
167
169
  throw new Error('Muse requires a host-bound output-schema control mapping');
168
170
  }
169
171
  if (!permissions || !['read-only', 'workspace-write', 'external'].includes(permissions.sandbox)
170
172
  || !Number.isSafeInteger(health?.maxSteps) || health.maxSteps < 1) throw new Error('Muse requires bounded steps and sandbox authority');
173
+ if ((permissions.approval ?? 'never') !== 'never') throw new Error('Muse direct worker approval requires never');
171
174
  // Trust is a host-owned, qualification-bound control, like the schema.
172
175
  // Brief contents and CLI defaults cannot opt into workspace instructions.
173
176
  let trust;
@@ -179,6 +182,7 @@ export class MuseAdapter extends ProviderAdapter {
179
182
  '--workspace', cwd, ...trust, '--approval-mode', 'never', '--approval-judge', 'off', '--max-model-steps', String(health.maxSteps),
180
183
  ...schema, '--prompt-file', briefPath];
181
184
  if (permissions.sandbox === 'read-only') args.push('--disable-write', '--disable-shell');
185
+ if (permissions.sandbox === 'external') args.push('--disable-sandbox');
182
186
  return args;
183
187
  }
184
188
 
@@ -361,4 +365,10 @@ export class MuseAdapter extends ProviderAdapter {
361
365
 
362
366
  registerProvider('muse', () => new MuseAdapter());
363
367
 
368
+ /** Installed worker module contract; legacy registration above remains supported. */
369
+ export const interfaceVersion = 1;
370
+ export function register(factory) {
371
+ factory('muse', () => new MuseAdapter());
372
+ }
373
+
364
374
  export default MuseAdapter;
@@ -1,6 +1,8 @@
1
+ import { RecoveryPolicy } from './consecutive-failure-circuit-breaker.mjs';
2
+
1
3
  /** A recovery probe is a conductor-owned request, never an uncontained worker. */
2
4
  export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000, maxOutputBytes = 65_536,
3
- onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now }) {
5
+ onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now, nowMs = () => performance.now() }) {
4
6
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 10_000
5
7
  || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 65_536) {
6
8
  throw new RangeError('Recovery probe exceeds the 10s/64KiB bounds');
@@ -19,6 +21,7 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
19
21
  return failed('probe-endpoint-unavailable', { operatorIntervention: true });
20
22
  }
21
23
  const controller = new AbortController();
24
+ const deadlineMs = nowMs() + timeoutMs;
22
25
  const timer = setTimeout(() => controller.abort(), Math.max(1, Math.floor(timeoutMs)));
23
26
  let aborted;
24
27
  const timeout = new Promise((_, reject) => {
@@ -30,14 +33,19 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
30
33
  timeout.catch(() => {});
31
34
  try {
32
35
  await onDispatch?.();
36
+ if (nowMs() >= deadlineMs) controller.abort();
33
37
  if (controller.signal.aborted) return failed('probe-timeout');
34
38
  try {
35
39
  // HEAD has no task, credentials, inference body or worker filesystem.
36
40
  // A successful endpoint check permits only one subsequent task canary.
37
41
  const response = await Promise.race([fetchImpl(endpoint, { method: 'HEAD', redirect: 'error', signal: controller.signal }), timeout]);
38
42
  let headerBytes = 16 + Buffer.byteLength(response.statusText ?? '');
39
- for (const [name, value] of response.headers) headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
40
43
  try {
44
+ if (nowMs() >= deadlineMs) { controller.abort(); return failed('probe-timeout'); }
45
+ for (const [name, value] of response.headers) {
46
+ headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
47
+ if (headerBytes > maxOutputBytes) return failed('probe-output-limit');
48
+ }
41
49
  if (headerBytes > maxOutputBytes) return failed('probe-output-limit');
42
50
  if (response.body) return failed('probe-body-unexpected');
43
51
  const success = response.status >= 200 && response.status <= 299;
@@ -45,7 +53,8 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
45
53
  if (response.status === 429) {
46
54
  const after = response.headers.get('retry-after');
47
55
  const seconds = after != null && /^\d+(?:\.\d+)?$/.test(after.trim()) ? Number(after) : null;
48
- const date = seconds === null && after ? Date.parse(after) : NaN;
56
+ const date = seconds === null && /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(after ?? '')
57
+ ? Date.parse(after) : NaN;
49
58
  const reset = response.headers.get('x-ratelimit-reset');
50
59
  const resetEpoch = reset != null && /^\d+(?:\.\d+)?$/.test(reset.trim()) ? Number(reset) * 1000 : NaN;
51
60
  const resetDelay = response.headers.get('ratelimit-reset');
@@ -53,7 +62,8 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
53
62
  Number.isFinite(resetEpoch) ? (resetEpoch - wallNowMs()) / 1000 : null,
54
63
  resetDelay != null && /^\d+(?:\.\d+)?$/.test(resetDelay.trim()) ? Number(resetDelay) : null]
55
64
  .filter(value => Number.isFinite(value) && value >= 0);
56
- return failed('probe-rate-limited', { retryAfterSeconds: delays.length ? Math.max(...delays) : null });
65
+ return failed('probe-rate-limited', { retryAfterSeconds: delays.length
66
+ ? Math.min(RecoveryPolicy.maxRetryAfterSeconds, Math.max(...delays)) : null });
57
67
  }
58
68
  return failed('probe-failed', { status: response.status });
59
69
  } finally {
@@ -123,7 +123,7 @@ export function createWorkerHealthObserver(options = {}) {
123
123
  const replayedKeys = new Set();
124
124
  const events = [];
125
125
  const gaps = [];
126
- let sequence = 0, generation = 0, steps = null, total = 0, lastCheck = started, maxGap = 0;
126
+ let sequence = 0, generation = options.observerGeneration ?? 0, steps = null, total = 0, lastCheck = started, maxGap = 0;
127
127
  let observed = null, activity = null, progress = null, heartbeat = null, handshakeAt = null, stalledAt = null;
128
128
  let state = 'launching', handshake = 'unobserved', confidence = 'unobserved', reasonCode = 'AWAITING_HANDSHAKE';
129
129
  let termination = 'running', transitionCount = 0, live = null, disconnected = false, finalized = false;
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Worker process isolation for Tier 0 direct dispatch (FR-OG-108, #2954).
3
3
  *
4
- * Direct workers must never share the conductor's cgroup: on the titan host
5
- * an oomd kill of one shared cgroup took the conductor session with it.
4
+ * Direct workers default to a separate cgroup: on the titan host an oomd
5
+ * kill of one shared cgroup took the conductor session with it. Explicit
6
+ * native-sandbox policy instead uses an owned process group and the CLI's
7
+ * sandbox, without a cgroup memory limit or descendant kill scope.
6
8
  * This module builds `systemd-run --user` launch arguments (own unit or
7
9
  * scope with `MemoryMax` and `MemorySwapMax=0`, PATH/HOME passthrough,
8
10
  * stdin `/dev/null`) and, where no systemd user manager exists, returns an
@@ -40,6 +42,50 @@ import { buildSystemdClientEnv, resolveSystemdClient, resolveSystemdRun, CONNECT
40
42
 
41
43
  const UNIT_SAFE = /[^A-Za-z0-9:_.@-]/g;
42
44
  const transportBootstrapVariables = ['NODE_OPTIONS', 'NODE_PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'LD_AUDIT', 'LD_DEBUG', 'LD_DEBUG_OUTPUT'];
45
+ const nativeSandboxProbes = new Map();
46
+ let nativeSandboxProbeSequence = 0;
47
+ export const NATIVE_SANDBOX_UNAVAILABLE = 'OG_NATIVE_SANDBOX_UNAVAILABLE';
48
+
49
+ export function workerNeedsNativeNamespaces(adapter, permissions = {}, platform = process.platform) {
50
+ return platform === 'linux' && adapter?.provider === 'codex'
51
+ && (permissions.transport ?? 'shell') === 'shell'
52
+ && ['read-only', 'workspace-write'].includes(permissions.sandbox ?? 'workspace-write');
53
+ }
54
+
55
+ export function nativeSandboxFailure(text) {
56
+ return /loopback: Failed RTM_NEWADDR: Operation not permitted|uid_map[^\n]*Operation not permitted/i.test(text);
57
+ }
58
+
59
+ export function nativeSandboxProbeMessage(result) {
60
+ return `${NATIVE_SANDBOX_UNAVAILABLE}: Native sandbox unavailable: restricted unprivileged user namespaces inside systemd user units (host AppArmor policy may restrict userns). Probe: ${String(result.detail ?? 'failed').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 512)}. Select parallelism.groups.<group>.execution.launchMode="native-sandbox" with a verified enforcing CLI sandbox, or configure a host AppArmor allowance for the systemd worker launch path.`;
61
+ }
62
+
63
+ /** No inference: exercise user/network namespaces through the worker's unit and client environment path. */
64
+ export function probeWorkerNativeSandbox({ spawn = spawnSync, env = process.env, envNames = ['PATH', 'HOME'], cwd,
65
+ cache = spawn === spawnSync ? nativeSandboxProbes : undefined } = {}) {
66
+ const key = JSON.stringify([cwd, [...envNames].sort()]);
67
+ if (cache?.has(key)) return cache.get(key);
68
+ let result;
69
+ try {
70
+ const managerEnvNames = resolveManagerEnvNames({ spawn });
71
+ const command = process.execPath;
72
+ const args = [fileURLToPath(import.meta.url), '--worker-environment', JSON.stringify(unitEnvironmentNames(envNames)),
73
+ '--', '/usr/bin/unshare', '--user', '--map-root-user', '--net', '/usr/bin/true'];
74
+ const argv = buildWorkerIsolationArgs({ unit: proposeWorkerUnitName(`sandbox-probe-${process.pid}-${++nativeSandboxProbeSequence}`),
75
+ command, args, cwd, env, envNames, managerEnvNames, memoryMaxGb: 0.25, attached: true,
76
+ extraProperties: ['RuntimeMaxSec=5s', 'TimeoutStopSec=1s'] });
77
+ const observed = spawn(resolveSystemdRun(), ['--collect', ...argv.slice(1)], {
78
+ encoding: 'utf8', timeout: 8000, maxBuffer: 4096, env: buildSystemdClientEnv({ env, envNames }) });
79
+ result = observed.status === 0 && !observed.error ? { ok: true, mode: 'systemd-unit' }
80
+ : { ok: false, mode: 'systemd-unit', code: NATIVE_SANDBOX_UNAVAILABLE,
81
+ detail: String(observed.error?.message ?? (String(observed.stderr ?? '').trim() || `exit ${observed.status}`)).replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 512) };
82
+ } catch (error) {
83
+ result = { ok: false, mode: 'systemd-unit', code: NATIVE_SANDBOX_UNAVAILABLE,
84
+ detail: String(error.message).replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 512) };
85
+ }
86
+ cache?.set(key, result);
87
+ return result;
88
+ }
43
89
 
44
90
  /** Derive a unit-safe `aiwg-worker-<name>-<epoch>` unit name. */
45
91
  export function proposeWorkerUnitName(name, nowSeconds = Math.floor(Date.now() / 1000)) {
@@ -340,6 +386,7 @@ export function launchIsolatedWorker({
340
386
  config,
341
387
  allowDegraded = false,
342
388
  isolation,
389
+ workerGroup,
343
390
  kind = 'unit',
344
391
  attached = false,
345
392
  spawn = spawnSync,
@@ -368,6 +415,15 @@ export function launchIsolatedWorker({
368
415
  return { launched: false, blocked: true, failures: resolvedGate.failures ?? [], gate: resolvedGate };
369
416
  }
370
417
  const detected = isolation ?? detectWorkerIsolation({ spawn });
418
+ const nativeSandbox = detected.mode === 'native-sandbox';
419
+ if (nativeSandbox) {
420
+ const execution = config?.parallelism?.groups?.[workerGroup]?.execution;
421
+ if (execution?.launchMode !== 'native-sandbox' || !['read-only', 'workspace-write'].includes(execution.sandbox)
422
+ || (execution.transport ?? 'shell') !== 'shell' || !attached || !ownedGroup) {
423
+ throw Object.assign(new Error('native-sandbox requires policy opt-in and an owned attached process group with an enforcing native sandbox'),
424
+ { code: 'OG_POLICY_RELAXATION' });
425
+ }
426
+ }
371
427
  if (detected.mode === 'degraded' && !allowDegraded) {
372
428
  return {
373
429
  launched: false,
@@ -376,7 +432,7 @@ export function launchIsolatedWorker({
376
432
  gate: resolvedGate,
377
433
  };
378
434
  }
379
- if (detected.mode === 'degraded') {
435
+ if (detected.mode === 'degraded' || nativeSandbox) {
380
436
  assertAbsolute('worker binary', command);
381
437
  if (cwd !== undefined) assertAbsolute('working directory', cwd);
382
438
  if (stdinPath !== undefined) assertAbsolute('stdin brief', stdinPath);
@@ -385,7 +441,7 @@ export function launchIsolatedWorker({
385
441
  if (!Array.isArray(args) || args.some(token => typeof token !== 'string' || token.includes('\0'))) {
386
442
  throw new Error('Worker isolation argv must be NUL-free strings');
387
443
  }
388
- // Degraded mode has no systemd unit to confine the environment, so this
444
+ // Process-group modes have no systemd unit to confine the environment, so this
389
445
  // module builds the allowlisted environment itself and passes it as the
390
446
  // child's *entire* environment (the `env` option replaces, never merges
391
447
  // with, the current process environment) — the functional equivalent of
@@ -441,12 +497,12 @@ export function launchIsolatedWorker({
441
497
  }
442
498
  const result = _runDispatchCommit(onDispatchCommit, () => {
443
499
  const child = spawnAsync(launchCommand, launchArgs, launchOptions);
444
- onSpawnAttempt?.({ mode: 'degraded', child, pid: child.pid });
500
+ onSpawnAttempt?.({ mode: detected.mode, child, pid: child.pid });
445
501
  const outcome = {
446
502
  launched: Number.isInteger(child.pid) && child.pid > 0,
447
- mode: 'degraded', child, pid: child.pid,
503
+ mode: detected.mode, child, pid: child.pid,
448
504
  ...(attached ? { transportSupervisor: true } : {}),
449
- diagnostic: `launched without resource confinement: ${detected.reason}`,
505
+ diagnostic: nativeSandbox ? 'Native CLI sandbox boundary; no cgroup resource confinement' : `launched without resource confinement: ${detected.reason}`,
450
506
  gate: resolvedGate,
451
507
  };
452
508
  // Preserve launch errors even when callers attach listeners later.
@@ -661,8 +717,8 @@ export function runWorkerTransport({ command, args, environment, record,
661
717
  }
662
718
 
663
719
  // Private attached transport entry, never a public CLI or legacy Ralph route.
664
- // The systemd unit owns this supervisor and its worker; cgroup exit evidence
665
- // includes descendants. Only the actual worker receives the allowed values.
720
+ // The unit or launcher-owned process group owns this supervisor and worker.
721
+ // Only unit cgroup exit evidence covers descendants. The worker receives only allowed values.
666
722
  if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === '--worker-environment') {
667
723
  process.umask(0o077);
668
724
  const names = JSON.parse(process.argv[3]);