chati-dev 4.5.27 → 4.5.29

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 (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +137 -20
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. package/src/wizard/questions.js +24 -12
@@ -0,0 +1,95 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { chmodSync, copyFileSync, lstatSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, isAbsolute, join } from 'node:path';
6
+ import { validateBrowserObservationPlan } from '@chati/planning';
7
+ export { validateBrowserObservationPlan } from '@chati/planning';
8
+ import { browserRuntimeLayout, copyObserverRuntime, observerRuntimeDigest } from './browser-runtime.js';
9
+ import { prepareExecutableSnapshot, assertExecutableSnapshot, disposeExecutableSnapshot } from './executable-snapshot.js';
10
+ import { runIsolatedBrowserSupervisor } from './spawner.js';
11
+ import { runDarwinBrowserObservation } from './darwin-browser-observer.js';
12
+ import { browserObservationCompleted } from './browser-process-facts.js';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
16
+ const bindingFor = (path) => ({ executable_path: realpathSync(path), executable_sha256: digest(readFileSync(path)) });
17
+ function fail(message, code = 'BROWSER_OBSERVATION_INVALID') { throw Object.assign(new Error(message), { code }); }
18
+ function text(value, limit = 8192) { return typeof value === 'string' && value.length > 0 && !value.includes('\0') && Buffer.byteLength(value) <= limit; }
19
+
20
+ function cleanupResources(runtime, browserImage, application) {
21
+ let cleanupError;
22
+ for (const cleanup of [() => { if (runtime) rmSync(runtime, { recursive: true, force: true }); },
23
+ () => { if (browserImage) disposeExecutableSnapshot(browserImage); },
24
+ () => { if (application) disposeExecutableSnapshot(application); }]) {
25
+ try { cleanup(); } catch (error) { cleanupError ??= error; }
26
+ }
27
+ if (cleanupError) throw cleanupError;
28
+ }
29
+
30
+ /** Real local observation only. It does not select a commit, approve a policy,
31
+ * retain artifacts or sign evidence. The canonical RAIL observer owns those gates.
32
+ */
33
+ export async function runIsolatedBrowserObservation(input = {}) {
34
+ if (!input || typeof input !== 'object' || !text(input.workingDir) || !isAbsolute(input.workingDir)) fail('browser requires an absolute disposable subject');
35
+ const { workingDir, ...planInput } = input;
36
+ const plan = structuredClone(validateBrowserObservationPlan(planInput));
37
+ if (process.platform === 'darwin' ? plan.application.listener_transport !== 'darwin_prebound_tcp_v1'
38
+ : Object.hasOwn(plan.application, 'listener_transport')) fail('application listener transport does not match this host', 'BROWSER_APPLICATION_LISTENER_TRANSPORT_REQUIRED');
39
+ const project = realpathSync(workingDir);
40
+ const nodeBinding = bindingFor(process.execPath);
41
+ let runtime; let application; let browserImage;
42
+ try {
43
+ const packagePath = require.resolve('playwright-core/package.json');
44
+ if (JSON.parse(readFileSync(packagePath, 'utf8')).version !== plan.browser.playwright_version) fail('installed Playwright does not match the approved driver version', 'BROWSER_DRIVER_DRIFT');
45
+ application = prepareExecutableSnapshot(plan.application.executable_binding, { projectDir: project, requireNativeImage: true });
46
+ browserImage = prepareExecutableSnapshot(plan.browser.executable_binding, { projectDir: project, requireNativeImage: true });
47
+ runtime = mkdtempSync(join(realpathSync(tmpdir()), 'chati-browser-observer-tools-'));
48
+ copyObserverRuntime(dirname(packagePath), join(runtime, 'playwright-core'), project);
49
+ copyFileSync(new URL('./browser-observer-driver.js', import.meta.url), join(runtime, 'observer-driver.mjs'));
50
+ const layout = browserRuntimeLayout(plan.browser.executable_binding.executable_path);
51
+ copyObserverRuntime(layout.source, join(runtime, 'chromium', layout.directory), project, { internalLinks: layout.internalLinks });
52
+ // Replace the launch image with bytes copied through the already pinned fd.
53
+ const browserPath = join(runtime, 'chromium', layout.executable);
54
+ const browserStat = lstatSync(browserPath);
55
+ if (!browserStat.isFile() || browserStat.nlink !== 1) fail('copied browser launch image must be a single regular file');
56
+ copyFileSync(browserImage.executablePath, browserPath);
57
+ copyFileSync(application.executablePath, join(runtime, 'application'));
58
+ chmodSync(join(runtime, 'application'), 0o500);
59
+ assertExecutableSnapshot(application); assertExecutableSnapshot(browserImage);
60
+ const runtime_digest = observerRuntimeDigest(runtime);
61
+ const config = { ...plan, working_dir: project, browser_file: layout.executable };
62
+ let facts; let darwin;
63
+ if (process.platform === 'darwin') {
64
+ darwin = await runDarwinBrowserObservation({ workingDir: project, runtimeDirectory: runtime,
65
+ browserFile: layout.executable, nodeBinding, plan });
66
+ facts = darwin.facts;
67
+ } else {
68
+ const supervisor = readFileSync(new URL('./browser-observer-supervisor.js', import.meta.url), 'utf8');
69
+ facts = await runIsolatedBrowserSupervisor({ workingDir: project, executableBinding: nodeBinding,
70
+ args: ['--input-type=module'], stdin: `const configuration = ${JSON.stringify(config)};\n${supervisor}`,
71
+ timeoutMs: plan.timeout_ms, observerDirectory: runtime });
72
+ }
73
+ if (observerRuntimeDigest(runtime) !== runtime_digest) fail('observer runtime changed during observation', 'BROWSER_RUNTIME_DRIFT');
74
+ const process_facts = Object.fromEntries(Object.entries(facts).filter(([key]) => !['stdout', 'stderr', 'stdout_base64', 'stderr_base64'].includes(key)));
75
+ const base = { schema_version: 1, evidence_class: 'real-browser-observation', tool: 'chati-playwright-api-v1',
76
+ runtime_digest, node_binding: nodeBinding, application_binding: plan.application.executable_binding,
77
+ browser_binding: plan.browser.executable_binding, playwright_version: plan.browser.playwright_version, process_facts,
78
+ ...(darwin ? { darwin_browser_binding: darwin.binding, application_stopped: false,
79
+ application_group_stopped: darwin.binding.application.process_group_stopped } : {}) };
80
+ if (facts.exit_code !== 0 || facts.signal !== null || facts.timed_out || facts.output_limit_exceeded || facts.input_error) {
81
+ return Object.freeze({ ...base, passed: false, error_code: facts.timed_out ? 'BROWSER_OBSERVATION_TIMEOUT' : 'BROWSER_OBSERVER_PROCESS_FAILED', steps: [], artifacts: [] });
82
+ }
83
+ let observed;
84
+ try { observed = JSON.parse(facts.stdout); } catch { fail('trusted browser observer returned invalid output', 'BROWSER_OBSERVER_OUTPUT_INVALID'); }
85
+ // Subprocess output describes the scenario, never the host's private
86
+ // process bindings. In particular it cannot replace Darwin app completion.
87
+ const result = { ...observed, ...base };
88
+ if (!browserObservationCompleted(result, { workingDir: project, ...plan })) {
89
+ return Object.freeze({ ...base, passed: false, error_code: 'BROWSER_OBSERVER_PROCESS_FAILED', steps: [], artifacts: [] });
90
+ }
91
+ return Object.freeze(result);
92
+ } finally {
93
+ cleanupResources(runtime, browserImage, application);
94
+ }
95
+ }
@@ -0,0 +1,75 @@
1
+ /** Typed browser completion, not a certificate or native authentication source.
2
+ * The canonical caller obtains these facts only from its private observer. */
3
+ import { createHash } from 'node:crypto';
4
+ import { readFileSync, realpathSync } from 'node:fs';
5
+ import { isAbsolute, join, relative, resolve } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { sha256 } from '@chati/core';
8
+ import { verificationProcessCompleted } from './verification-process-facts.js';
9
+
10
+ function invalid() { throw Object.assign(new Error('browser requires bound observer, application and endpoint completion'), { code: 'BROWSER_COMPLETION_INVALID' }); }
11
+ function canonicalPath(value) { return typeof value === 'string' && isAbsolute(value) && resolve(value) === value
12
+ && !Array.from(value).some(character => character.codePointAt(0) < 32 || character.codePointAt(0) === 127); }
13
+ function contains(root, child) { const part = relative(root, child); return !part || (part !== '..' && !part.startsWith('../') && !isAbsolute(part)); }
14
+ const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
15
+
16
+ export function darwinBrowserObserverInvocation({ runtimeDirectory, workingDir, browserFile, proxyPort, nodeBinding, plan }) {
17
+ if (!canonicalPath(runtimeDirectory) || !canonicalPath(workingDir) || typeof browserFile !== 'string'
18
+ || isAbsolute(browserFile) || browserFile.split('/').some(part => !part || part === '.' || part === '..')
19
+ || !Number.isInteger(proxyPort) || proxyPort < 1024 || proxyPort > 65535) invalid();
20
+ const config = { hostname: plan.hostname, application: { port: plan.application.port }, timeout_ms: plan.timeout_ms, steps: plan.steps };
21
+ const options = { executablePath: join(runtimeDirectory, 'chromium', browserFile),
22
+ proxy: { server: `http://127.0.0.1:${proxyPort}`, bypass: '<-loopback>' },
23
+ args: ['--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE 127.0.0.1', '--use-mock-keychain'] };
24
+ const stdin = `import { chromium } from ${JSON.stringify(pathToFileURL(join(runtimeDirectory, 'playwright-core/index.mjs')).href)};
25
+ import { observeBrowserScenario } from ${JSON.stringify(pathToFileURL(join(runtimeDirectory, 'observer-driver.mjs')).href)};
26
+ const observed = await observeBrowserScenario({ chromium, config: ${JSON.stringify(config)}, launchOptions: ${JSON.stringify(options)}, isApplicationAlive: () => true });
27
+ process.stdout.write(JSON.stringify(observed));\n`;
28
+ // The host's private application lifetime signal aborts this process on app
29
+ // death; the driver does not pretend to authenticate another process itself.
30
+ return { workingDir, executableBinding: nodeBinding, args: ['--input-type=module'], stdin, timeoutMs: plan.timeout_ms };
31
+ }
32
+
33
+ export function browserObservationCompleted(observed, expected) {
34
+ if (observed?.process_facts?.containment_kind !== 'darwin_scoped_process_group_v1') {
35
+ return observed?.process_facts?.containment_kind === 'bubblewrap_pid_namespace_v1'
36
+ && observed.process_facts.process_tree_stopped === true && observed.application_stopped === true
37
+ && !Object.hasOwn(observed, 'darwin_browser_binding');
38
+ }
39
+ const binding = observed.darwin_browser_binding;
40
+ const fields = ['schema_version', 'runtime_directory', 'observer_working_dir', 'browser_file', 'proxy_port', 'base_bundle_id', 'application'];
41
+ if (!binding || Object.keys(binding).length !== fields.length || fields.some(key => !Object.hasOwn(binding, key))
42
+ || binding.schema_version !== 1 || !canonicalPath(expected?.workingDir)
43
+ || !canonicalPath(binding.runtime_directory) || !canonicalPath(binding.observer_working_dir)
44
+ || [binding.runtime_directory, binding.observer_working_dir].some(path => contains(path, expected.workingDir) || contains(expected.workingDir, path))
45
+ || contains(binding.runtime_directory, binding.observer_working_dir) || contains(binding.observer_working_dir, binding.runtime_directory)
46
+ || typeof binding.base_bundle_id !== 'string' || binding.base_bundle_id.length > 200
47
+ || !/^(?:[A-Za-z0-9-]+\.)+[A-Za-z0-9-]+$/.test(binding.base_bundle_id)
48
+ || expected.application?.listener_transport !== 'darwin_prebound_tcp_v1'
49
+ || observed.application_stopped !== false || observed.application_group_stopped !== true) invalid();
50
+ const nodeBinding = { executable_path: realpathSync(process.execPath),
51
+ executable_sha256: createHash('sha256').update(readFileSync(process.execPath)).digest('hex') };
52
+ if (sha256(observed.node_binding) !== sha256(nodeBinding)
53
+ || sha256(observed.application_binding) !== sha256(expected.application.executable_binding)
54
+ || sha256(observed.browser_binding) !== sha256(expected.browser.executable_binding)) invalid();
55
+ const invocation = darwinBrowserObserverInvocation({ runtimeDirectory: binding.runtime_directory,
56
+ workingDir: binding.observer_working_dir, browserFile: binding.browser_file,
57
+ proxyPort: binding.proxy_port, nodeBinding, plan: expected });
58
+ if (!verificationProcessCompleted(observed.process_facts, invocation)
59
+ || observed.process_facts.exit_code !== 0 || observed.process_facts.signal !== null
60
+ || observed.process_facts.timed_out !== false || observed.process_facts.output_limit_exceeded !== false
61
+ || observed.process_facts.input_error !== null) invalid();
62
+ const app = binding.application;
63
+ if (!app || app.listener_transport !== expected.application.listener_transport
64
+ || !verificationProcessCompleted(app, { workingDir: expected.workingDir, executableBinding: expected.application.executable_binding,
65
+ args: expected.application.args, stdin: expected.application.stdin }) || app.error_code !== null || app.signal !== null
66
+ || app.timed_out !== false || app.output_limit_exceeded !== false || app.input_error !== null || app.requested_stop !== true
67
+ || app.darwin_verification_binding.completion.supervisor_signal !== 15
68
+ || !(app.darwin_verification_binding.completion.signal === 15 || app.darwin_verification_binding.completion.exit_code === 0)
69
+ || app.endpoint_proof?.nonce !== app.darwin_verification_binding.ready.nonce || app.endpoint_proof.closed !== true
70
+ || !hash(app.endpoint_proof.proof_chain_sha256) || !Number.isSafeInteger(app.endpoint_proof.positive_proofs)
71
+ || !Number.isSafeInteger(app.proxy?.connections_proved) || app.proxy.connections_proved < 2
72
+ || app.endpoint_proof.positive_proofs < app.proxy.connections_proved || app.proxy.owner_rejections !== 0
73
+ || app.proxy.policy_violations !== 0 || app.proxy.closed !== true || app.proxy.timed_out !== false || app.proxy.active_connections !== 0) invalid();
74
+ return true;
75
+ }
@@ -0,0 +1,131 @@
1
+ // Private tool assembly. Never copy the application workspace into the observer.
2
+ import { createHash } from 'node:crypto';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { cpSync, lstatSync, readFileSync, readdirSync, readlinkSync, realpathSync } from 'node:fs';
5
+ import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
6
+ import { sha256 } from '@chati/core';
7
+ import { installationAuthorityStateDirectory } from '../installer-v2/installation-authority.js';
8
+
9
+ const digest = bytes => createHash('sha256').update(bytes).digest('hex');
10
+ function fail(message) { throw Object.assign(new Error(message), { code: 'BROWSER_OBSERVATION_INVALID' }); }
11
+ function inside(root, target) { const part = relative(root, target); return part === '' || (part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)); }
12
+
13
+ export function browserRuntimeLayout(executable, platform = process.platform) {
14
+ if (platform === 'darwin' && executable.split(sep).some(part => part.endsWith('.app'))) {
15
+ const source = dirname(dirname(dirname(executable)));
16
+ if (basename(dirname(executable)) !== 'MacOS' || basename(dirname(dirname(executable))) !== 'Contents'
17
+ || !basename(source).endsWith('.app')) fail('unsupported browser app bundle layout');
18
+ return { source, directory: basename(source), executable: join(basename(source), relative(source, executable)), internalLinks: true };
19
+ }
20
+ return { source: dirname(executable), directory: '', executable: basename(executable), internalLinks: false };
21
+ }
22
+
23
+ export function parseDarwinBrowserBundleInfo(info, executableName) {
24
+ if (!info || info.CFBundlePackageType !== 'APPL' || info.CFBundleExecutable !== executableName
25
+ || typeof info.CFBundleIdentifier !== 'string' || info.CFBundleIdentifier.length > 200
26
+ || !/^(?:[A-Za-z0-9-]+\.)+[A-Za-z0-9-]+$/.test(info.CFBundleIdentifier)) {
27
+ fail('Darwin browser requires the intact main Chromium application bundle identity');
28
+ }
29
+ return info.CFBundleIdentifier;
30
+ }
31
+
32
+ // Qualified Darwin arm64 Playwright Chromium153.0.8010.12 headless artifact.
33
+ // BaseBundleID is a compiled rendezvous namespace, NOT its codesign identifier
34
+ // or CFBundleIdentifier (this flat image has no Info.plist). Future artifacts
35
+ // need qualification; neither a filename nor a caller-supplied hash is trust.
36
+ const DARWIN_HEADLESS_NAMESPACES = Object.freeze({
37
+ a0bfe7b4da4787b66058477d696cd1d09065d25f06a548947722b9af77ee8282: 'org.chromium.Chromium',
38
+ });
39
+
40
+ export function darwinHeadlessRendezvousNamespace(executable, executableSha256) {
41
+ if (browserRuntimeLayout(executable, 'darwin').internalLinks || basename(executable) !== 'chrome-headless-shell'
42
+ || !Object.hasOwn(DARWIN_HEADLESS_NAMESPACES, executableSha256)) {
43
+ fail('Darwin headless browser artifact has no qualified rendezvous namespace');
44
+ }
45
+ return DARWIN_HEADLESS_NAMESPACES[executableSha256];
46
+ }
47
+
48
+ export function readDarwinBrowserRendezvousNamespace(executable) {
49
+ if (process.platform !== 'darwin') fail('Darwin bundle inspection requires the actual Mac');
50
+ const layout = browserRuntimeLayout(executable, 'darwin');
51
+ if (!layout.internalLinks) {
52
+ const stat = lstatSync(executable);
53
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024 * 1024) fail('headless browser must be a bounded regular copied image');
54
+ return darwinHeadlessRendezvousNamespace(executable, digest(readFileSync(executable)));
55
+ }
56
+ const infoPath = join(layout.source, 'Contents', 'Info.plist');
57
+ const stat = lstatSync(infoPath);
58
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 65536) fail('browser bundle metadata must be a bounded regular copied file');
59
+ const contents = execFileSync('/usr/bin/plutil', ['-convert', 'json', '-o', '-', infoPath], {
60
+ encoding: 'utf8', timeout: 5000, maxBuffer: 65536, env: { PATH: '/usr/bin:/bin' }, stdio: ['ignore', 'pipe', 'pipe'],
61
+ });
62
+ return parseDarwinBrowserBundleInfo(JSON.parse(contents), basename(executable));
63
+ }
64
+
65
+ function internalLink(root, path) {
66
+ const link = readlinkSync(path);
67
+ if (!link || isAbsolute(link)) fail('observer runtime link must be relative and internal');
68
+ let resolved;
69
+ try { resolved = realpathSync(path); } catch { fail('observer runtime link is dangling or cyclic'); }
70
+ if (!inside(root, resolved) || inside(resolved, path)) fail('observer runtime link escapes its bundle or forms an ancestor cycle');
71
+ const target = lstatSync(resolved);
72
+ if (!target.isFile() && !target.isDirectory()) fail('observer runtime link targets a special file');
73
+ return { link, resolved, directory: target.isDirectory() };
74
+ }
75
+
76
+ export function copyObserverRuntime(source, target, project, { internalLinks = false } = {}) {
77
+ const physical = realpathSync(source);
78
+ const authority = installationAuthorityStateDirectory();
79
+ if (inside(project, physical) || inside(physical, project)
80
+ || inside(authority, physical) || inside(physical, authority)) fail('observer runtime cannot overlap tested application code or private authority storage');
81
+ let bytes = 0; let files = 0;
82
+ cpSync(physical, target, { recursive: true, errorOnExist: true, force: false, verbatimSymlinks: true, filter(path) {
83
+ const stat = lstatSync(path);
84
+ if (stat.isSymbolicLink() && internalLinks) internalLink(physical, path);
85
+ else if (!stat.isDirectory() && !stat.isFile()) fail('observer runtime must not contain symlinks or special files');
86
+ files += 1; bytes += stat.isFile() ? stat.size : 0;
87
+ if (files > 20000 || bytes > 1024 * 1024 * 1024) fail('observer runtime exceeds the bounded tool-copy budget');
88
+ return true;
89
+ } });
90
+ // Recheck the copy itself before any process can execute from it. Relative
91
+ // framework links must resolve within the copied bundle, never the source.
92
+ observerRuntimeDigest(target);
93
+ }
94
+
95
+ export function observerRuntimeDigest(path) {
96
+ path = realpathSync(path);
97
+ const entries = [];
98
+ const directories = new Map();
99
+ function visit(directory) {
100
+ const edges = []; directories.set(directory, edges);
101
+ for (const name of readdirSync(directory).sort()) {
102
+ const file = join(directory, name); const stat = lstatSync(file);
103
+ if (stat.isDirectory()) { edges.push(file); visit(file); }
104
+ else if (stat.isFile() && stat.nlink === 1) entries.push({ path: relative(path, file), mode: stat.mode & 0o777, sha256: digest(readFileSync(file)) });
105
+ else if (stat.isSymbolicLink()) {
106
+ const link = internalLink(path, file);
107
+ entries.push({ path: relative(path, file), kind: 'symlink', target: link.link });
108
+ if (link.directory) edges.push(link.resolved);
109
+ }
110
+ else fail('observer runtime copy changed identity');
111
+ }
112
+ }
113
+ visit(path);
114
+ // A/B -> B and B/A -> A are individually resolvable internal links, yet
115
+ // together create an unbounded traversal. Check the directory graph without
116
+ // following links while copying/hashing; shared DAG destinations are valid.
117
+ const active = new Set(); const completed = new Set();
118
+ const stack = [{ directory: path, index: 0 }]; active.add(path);
119
+ while (stack.length) {
120
+ const frame = stack.at(-1); const edges = directories.get(frame.directory);
121
+ if (!edges) fail('observer runtime directory target changed identity');
122
+ if (frame.index === edges.length) {
123
+ active.delete(frame.directory); completed.add(frame.directory); stack.pop(); continue;
124
+ }
125
+ const child = edges[frame.index++];
126
+ if (active.has(child)) fail('observer runtime contains a directory cycle');
127
+ if (completed.has(child)) continue;
128
+ active.add(child); stack.push({ directory: child, index: 0 });
129
+ }
130
+ return sha256(entries);
131
+ }
@@ -0,0 +1,213 @@
1
+ /** Scoped Darwin authority policy. Kernel acceptance and effective permissions
2
+ * must be exercised on macOS; generating SBPL is not containment evidence. */
3
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { PROVIDER_CONTROL_PLANE_DIRECTORIES } from '@chati/core';
5
+ export { PROVIDER_CONTROL_PLANE_DIRECTORIES };
6
+ function fail(message) { throw Object.assign(new Error(message), { code: 'DARWIN_AUTHORITY_POLICY_INVALID' }); }
7
+ function path(value) {
8
+ if (typeof value !== 'string' || !isAbsolute(value) || resolve(value) !== value
9
+ || Buffer.byteLength(value) > 900
10
+ || Array.from(value).some(character => character.codePointAt(0) < 32 || character.codePointAt(0) === 127)) fail('policy paths must be bounded canonical absolute paths');
11
+ return value;
12
+ }
13
+ function quote(value) { return JSON.stringify(path(value)); }
14
+ function contains(root, candidate) {
15
+ const entry = relative(root, candidate);
16
+ return !entry || (entry !== '..' && !entry.startsWith('../') && !isAbsolute(entry));
17
+ }
18
+ function overlaps(left, right) { return contains(left, right) || contains(right, left); }
19
+ const literal = value => `(literal ${quote(value)})`;
20
+ const subtree = value => `(subpath ${quote(value)})`;
21
+ function rule(action, operations, filters) {
22
+ if (!filters.length) return '';
23
+ return `(${action} ${operations} ${[...new Set(filters)].join(' ')})\n`;
24
+ }
25
+
26
+ // Explicit runtime roots only. In particular, /System would also expose the
27
+ // writable Data volume. No operator home, provider state, socket or credential
28
+ // service is made available to builder-controlled verification scripts.
29
+ const VERIFICATION_SYSTEM_ROOTS = Object.freeze(['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/lib', '/usr/share',
30
+ '/System/Library', '/Library/Apple/System/Library', '/System/Cryptexes', '/System/Volumes/Preboot/Cryptexes']);
31
+
32
+ // Application role only. This never grants outbound access, even to the trusted
33
+ // proxy. The observer has its own sandbox and private writable runtime.
34
+ export function darwinBrowserApplicationProfile({ applicationPort, observerDirectory, ...inputs }) {
35
+ if (!Number.isInteger(applicationPort) || applicationPort < 1024 || applicationPort > 65535) fail('browser application requires its frozen unprivileged port');
36
+ path(observerDirectory);
37
+ if (observerDirectory === '/' || [...VERIFICATION_SYSTEM_ROOTS, inputs.project, inputs.temporaryDirectory,
38
+ inputs.snapshotDirectory, inputs.launcherDirectory, inputs.authorityDirectory].some(value => overlaps(path(value), observerDirectory))) {
39
+ fail('browser observer tools must be separate from application and system roots');
40
+ }
41
+ const profile = darwinVerificationProfile({ ...inputs, maskedReadPaths: [...(inputs.maskedReadPaths ?? []), observerDirectory] })
42
+ // The already-bound fd11 is the capability. No new bind or implicit listen
43
+ // is allowed: localhost in SBPL also matches wildcard bind on actual XNU.
44
+ + `(allow network-inbound (local tcp "localhost:${applicationPort}"))\n`;
45
+ if (Buffer.byteLength(profile) > 65536) fail('browser application policy exceeds its bounded native profile size');
46
+ return profile;
47
+ }
48
+
49
+ // Observer role only. The host derives Chromium's BaseBundleID namespace from
50
+ // its runtime (qualified headless artifact or main application bundle), never
51
+ // from an application-selected Mach name, environment variable or plan field.
52
+ export function darwinBrowserObserverProfile({ proxyPort, observerDirectory, applicationProject, baseBundleId, ...inputs }) {
53
+ if (!Number.isInteger(proxyPort) || proxyPort < 1024 || proxyPort > 65535
54
+ || typeof baseBundleId !== 'string' || baseBundleId.length > 200
55
+ || !/^(?:[A-Za-z0-9-]+\.)+[A-Za-z0-9-]+$/.test(baseBundleId)) fail('browser observer requires its private proxy and sealed bundle identity');
56
+ const roots = [...VERIFICATION_SYSTEM_ROOTS, inputs.project, inputs.temporaryDirectory,
57
+ inputs.snapshotDirectory, inputs.launcherDirectory, inputs.authorityDirectory].map(path);
58
+ for (const value of [observerDirectory, applicationProject]) {
59
+ if (path(value) === '/' || roots.some(other => overlaps(other, value))) fail('observer and application roots must be separate from private/system roots');
60
+ }
61
+ if (overlaps(observerDirectory, applicationProject)) fail('observer tools cannot overlap application code');
62
+ const protectedPaths = [observerDirectory];
63
+ for (let parent = dirname(observerDirectory); ; parent = dirname(parent)) {
64
+ protectedPaths.push(parent);
65
+ if (parent === dirname(parent)) break;
66
+ }
67
+ let profile = darwinVerificationProfile({ ...inputs, maskedReadPaths: [...(inputs.maskedReadPaths ?? []), applicationProject] });
68
+ profile += rule('allow', 'file-read* file-map-executable', [subtree(observerDirectory)]);
69
+ profile += `(allow network-outbound (remote tcp "localhost:${proxyPort}"))\n`;
70
+ // Anonymous socketpair IPC only: no pathname bind/connect rule is added.
71
+ profile += '(allow system-socket (socket-domain AF_UNIX))\n';
72
+ // Chromium's inner sandbox limits lookup to the actual browser PID. Its
73
+ // rendezvous server selects registered child ports by kernel audit token.
74
+ // This outer regex is not itself per-invocation identity authentication.
75
+ // #"..." is an SBPL regex literal, not a JSON-decoded string. Literal-dot
76
+ // character classes avoid duplicating the escape layer at this boundary.
77
+ const rendezvous = `^${baseBundleId.replaceAll('.', '[.]')}[.]MachPortRendezvousServer[.][1-9][0-9]*$`;
78
+ profile += `(allow mach-register mach-lookup (global-name-regex #${JSON.stringify(rendezvous)}))\n`;
79
+ profile += '(allow mach-lookup (global-name "com.apple.logd") (global-name "com.apple.system.logger") (global-name "com.apple.system.opendirectoryd.libinfo") (global-name "com.apple.system.opendirectoryd.membership") (global-name "com.apple.FontObjectsServer") (global-name "com.apple.fonts"))\n';
80
+ // Chromium registers its native power monitor during startup. This exact
81
+ // class capability is NOT read-only: a compromised observer could affect
82
+ // host sleep/energy. It belongs to the immutable observer family (Node,
83
+ // driver, browser and descendants), never to the separate application.
84
+ // Chromium's own child sandbox remains enabled; no global OS setting changes.
85
+ profile += '(allow iokit-open-user-client (iokit-user-client-class "RootDomainUserClient"))\n';
86
+ profile += '(allow file-read-data (literal "/private/etc/passwd"))\n';
87
+ profile += rule('deny', 'file-read* file-map-executable', ['/Library/Keychains', '/System/Library/Keychains'].map(subtree));
88
+ profile += rule('deny', 'file-write*', [subtree(observerDirectory), ...protectedPaths.map(literal)]);
89
+ if (Buffer.byteLength(profile) > 65536) fail('browser observer policy exceeds its bounded native profile size');
90
+ return profile;
91
+ }
92
+
93
+ export function darwinVerificationProfile({ project, temporaryDirectory, snapshotDirectory, launcherDirectory,
94
+ launcherPath, executablePath, authorityDirectory, maskedReadPaths = [], subjectReadOnly = false }) {
95
+ if (typeof subjectReadOnly !== 'boolean') fail('verification subject access must be an explicit boolean');
96
+ const roots = [project, temporaryDirectory, snapshotDirectory, launcherDirectory, authorityDirectory];
97
+ roots.forEach(value => { if (path(value) === '/') fail('verification roots must not be the filesystem root'); });
98
+ for (const [index, value] of roots.entries()) {
99
+ if (roots.slice(index + 1).some(other => overlaps(value, other))
100
+ || VERIFICATION_SYSTEM_ROOTS.some(other => overlaps(value, other))) fail('verification roots must be separate from each other and system runtime roots');
101
+ }
102
+ if (!contains(launcherDirectory, path(launcherPath)) || !contains(snapshotDirectory, path(executablePath))) fail('verification executables must belong to sealed snapshots');
103
+ if (!Array.isArray(maskedReadPaths)) fail('verification read masks must be explicit');
104
+ const masked = [...new Set(maskedReadPaths.map(path))];
105
+ if (masked.some(value => value === '/' || roots.slice(0, 4).some(root => overlaps(value, root)))) fail('verification masks overlap required runtime roots');
106
+ const controlPlane = PROVIDER_CONTROL_PLANE_DIRECTORIES.map(name => join(project, name));
107
+ const protectedTrees = [snapshotDirectory, launcherDirectory, authorityDirectory, ...controlPlane, ...masked];
108
+ const ancestors = new Set([project, temporaryDirectory]);
109
+ for (const target of [...roots, ...protectedTrees]) {
110
+ for (let parent = dirname(target); ; parent = dirname(parent)) {
111
+ ancestors.add(parent);
112
+ if (parent === dirname(parent)) break;
113
+ }
114
+ }
115
+ let profile = '(version 1)\n(deny default)\n';
116
+ profile += '(allow process-fork process-exec)\n(allow process-info* (target same-sandbox))\n(allow signal (target same-sandbox))\n';
117
+ profile += '(allow file-read-metadata)\n';
118
+ // dyld opens the root directory while locating its shared cache. This literal
119
+ // grant permits directory enumeration only, not descendant reads or writes.
120
+ profile += '(allow file-read-data (literal "/"))\n';
121
+ profile += rule('allow', 'file-read* file-map-executable', [...VERIFICATION_SYSTEM_ROOTS,
122
+ project, temporaryDirectory, snapshotDirectory, launcherDirectory].map(subtree));
123
+ profile += '(allow file-read-data (literal "/dev/random") (literal "/dev/urandom"))\n';
124
+ profile += '(allow file-ioctl file-read-data file-write-data (literal "/dev/null"))\n';
125
+ // uv_os_uname reads this public kernel version; Node imports node:os while
126
+ // loading other builtins and cannot initialize when this exact query fails.
127
+ profile += '(allow sysctl-read (sysctl-name "kern.version"))\n';
128
+ profile += '(allow sysctl-read (sysctl-name-prefix "hw.") (sysctl-name "kern.ostype") (sysctl-name "kern.osrelease") (sysctl-name "kern.osversion") (sysctl-name "kern.osproductversion") (sysctl-name "kern.maxfilesperproc") (sysctl-name "kern.argmax") (sysctl-name "kern.hostname") (sysctl-name "kern.tcsm_available") (sysctl-name "kern.tcsm_enable"))\n';
129
+ profile += rule('allow', 'file-write*', (subjectReadOnly ? [temporaryDirectory] : [project, temporaryDirectory]).map(subtree));
130
+ profile += rule('deny', 'file-read* file-map-executable', [authorityDirectory, ...controlPlane, ...masked].map(subtree));
131
+ profile += rule('deny', 'file-write*', [...protectedTrees.map(subtree), ...[...ancestors].map(literal)]);
132
+ if (Buffer.byteLength(profile) > 65536) fail('verification policy exceeds its bounded native profile size');
133
+ return profile;
134
+ }
135
+
136
+ export function darwinAuthorityProfile({
137
+ project, stateDirectory, temporaryDirectory, snapshotDirectory, launcherDirectory,
138
+ launcherPath, executablePath, supportingExecutablePaths = [], authorityDirectory,
139
+ promptDirectory, writeScopes, maskedReadPaths = [], councilToolIsolation = false, homeDirectory,
140
+ }) {
141
+ const roots = { project, stateDirectory, temporaryDirectory, snapshotDirectory, launcherDirectory, authorityDirectory, homeDirectory };
142
+ for (const [key, value] of Object.entries(roots)) if (path(value) === '/') fail(`${key} must not be the filesystem root`);
143
+ path(launcherPath); path(executablePath);
144
+ if (!contains(launcherDirectory, launcherPath) || !contains(snapshotDirectory, executablePath)) fail('native executables must belong to their sealed snapshots');
145
+ if (!Array.isArray(supportingExecutablePaths) || supportingExecutablePaths.some(value => !contains(snapshotDirectory, path(value)))) fail('supporting executables must belong to the provider snapshot');
146
+ if (typeof councilToolIsolation !== 'boolean' || !Array.isArray(maskedReadPaths)
147
+ || !writeScopes || typeof writeScopes.fullProject !== 'boolean' || !Array.isArray(writeScopes.entries)) fail('explicit scoped policy inputs are required');
148
+ if (writeScopes.fullProject && writeScopes.entries.length) fail('full-project and partial write scopes cannot be combined');
149
+ // Exclude accidental broad grants around the private authority and snapshots.
150
+ const privateRoots = [stateDirectory, temporaryDirectory, snapshotDirectory, launcherDirectory, authorityDirectory];
151
+ for (let index = 0; index < privateRoots.length; index++) {
152
+ if (overlaps(project, privateRoots[index])) fail('private runtime roots cannot overlap the project');
153
+ for (const other of privateRoots.slice(index + 1)) if (overlaps(privateRoots[index], other)) fail('private runtime roots must be distinct');
154
+ }
155
+ const controlPlane = PROVIDER_CONTROL_PLANE_DIRECTORIES.map(name => join(project, name));
156
+ const scopedWrites = writeScopes.fullProject ? [subtree(project)] : writeScopes.entries.map(entry => {
157
+ path(entry?.target);
158
+ if (!['file', 'directory'].includes(entry.kind) || entry.target === project || !contains(project, entry.target)
159
+ || controlPlane.some(value => overlaps(value, entry.target))) fail('write scope escapes the product tree or overlaps its control plane');
160
+ return entry.kind === 'file' ? literal(entry.target) : subtree(entry.target);
161
+ });
162
+ const masked = [...new Set(maskedReadPaths.map(path))];
163
+ for (const denied of masked) {
164
+ if (denied === '/' || overlaps(denied, project) || privateRoots.slice(0, 4).some(value => overlaps(denied, value))) {
165
+ fail('masked paths must not overlap project or required private runtime roots');
166
+ }
167
+ }
168
+ if (promptDirectory) {
169
+ path(promptDirectory);
170
+ if (privateRoots.some(value => overlaps(value, promptDirectory)) || overlaps(project, promptDirectory)) fail('private prompt must be separate from writable/private runtime roots');
171
+ }
172
+ const protectedTrees = [...controlPlane, snapshotDirectory, launcherDirectory, authorityDirectory,
173
+ ...masked, ...(promptDirectory ? [promptDirectory] : [])];
174
+ const ancestors = new Set([project, stateDirectory, temporaryDirectory]);
175
+ for (const target of [...protectedTrees, ...writeScopes.entries.map(entry => entry.target)]) {
176
+ for (let parent = dirname(target); ; parent = dirname(parent)) {
177
+ ancestors.add(parent);
178
+ if (parent === dirname(parent)) break;
179
+ }
180
+ }
181
+ const executableFilters = [launcherPath, executablePath, ...supportingExecutablePaths].map(literal);
182
+ let profile = '(version 1)\n(deny default)\n';
183
+ profile += '(allow process-fork)\n(allow process-info* (target same-sandbox))\n(allow signal (target same-sandbox))\n';
184
+ // No task-port grant: the provider must not inspect/modify its supervisor.
185
+ profile += councilToolIsolation ? rule('allow', 'process-exec', executableFilters) : '(allow process-exec)\n';
186
+ profile += '(allow file-read* file-map-executable)\n';
187
+ profile += '(allow sysctl-read (sysctl-name-prefix "hw.") (sysctl-name "kern.ostype") (sysctl-name "kern.osrelease") (sysctl-name "kern.osversion") (sysctl-name "kern.osproductversion") (sysctl-name "kern.maxfilesperproc") (sysctl-name "kern.argmax") (sysctl-name "kern.hostname") (sysctl-name "kern.tcsm_available") (sysctl-name "kern.tcsm_enable"))\n';
188
+ profile += '(allow sysctl-write (sysctl-name "kern.tcsm_enable"))\n';
189
+ profile += '(allow sysctl-read (sysctl-name "kern.version"))\n';
190
+ profile += '(allow mach-lookup (global-name "com.apple.SecurityServer") (global-name "com.apple.securityd.xpc") (global-name "com.apple.trustd.agent") (global-name "com.apple.system.opendirectoryd.libinfo") (global-name "com.apple.system.opendirectoryd.membership") (global-name "com.apple.system.logger") (global-name "com.apple.logd"))\n';
191
+ // IP transport is allowed for providers and product tests. Host Unix sockets,
192
+ // Apple Events, unrestricted Mach services and privileged task ports are not.
193
+ profile += '(allow network-outbound (remote ip "*:*"))\n(allow network-bind network-inbound (local ip "*:*"))\n';
194
+ // Apple's DNS client uses this system-owned Unix endpoint. This does not
195
+ // grant SSH-agent, Docker, arbitrary XPC, or other host socket access.
196
+ profile += '(allow system-socket (socket-domain AF_UNIX))\n(allow network-outbound (remote unix-socket (literal "/private/var/run/mDNSResponder")))\n';
197
+ profile += '(allow file-ioctl file-read-data file-write-data (literal "/dev/null"))\n';
198
+ profile += rule('allow', 'file-write*', [...scopedWrites, subtree(stateDirectory), subtree(temporaryDirectory)]);
199
+ if (councilToolIsolation) {
200
+ const homes = [...new Set(['/Users', '/var/root', '/private/var/root', homeDirectory])].map(subtree);
201
+ const visible = [project, stateDirectory, temporaryDirectory, snapshotDirectory, launcherDirectory,
202
+ ...(promptDirectory ? [promptDirectory] : [])];
203
+ profile += rule('deny', 'file-read* file-map-executable', [
204
+ `(require-all (require-any ${homes.join(' ')}) ${visible.map(value => `(require-not ${subtree(value)})`).join(' ')})`,
205
+ ...['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/local/bin', '/usr/local/sbin', '/opt/homebrew/bin', '/opt/homebrew/sbin'].map(subtree),
206
+ ]);
207
+ }
208
+ // Denials remain last. A later write allow must not reopen a protected path.
209
+ profile += rule('deny', 'file-read* file-map-executable', [authorityDirectory, ...masked].map(subtree));
210
+ profile += rule('deny', 'file-write*', [...protectedTrees.map(subtree), ...[...ancestors].map(literal)]);
211
+ if (Buffer.byteLength(profile) > 65536) fail('scoped policy exceeds its bounded native profile size');
212
+ return profile;
213
+ }