chati-dev 4.5.16 → 4.5.28

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 (122) hide show
  1. package/README.md +8 -3
  2. package/bin/chati.js +124 -66
  3. package/framework/agents/build/dev.md +5 -5
  4. package/framework/agents/deploy/devops.md +7 -7
  5. package/framework/agents/discover/brief.md +4 -4
  6. package/framework/agents/discover/brownfield-wu.md +3 -3
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +4 -4
  10. package/framework/agents/plan/phases.md +2 -2
  11. package/framework/agents/plan/tasks.md +2 -2
  12. package/framework/agents/plan/ux.md +2 -2
  13. package/framework/agents/quality/qa-implementation.md +11 -10
  14. package/framework/agents/quality/qa-planning.md +3 -3
  15. package/framework/agents/quality/qa-visual.md +1 -1
  16. package/framework/config.yaml +3 -3
  17. package/framework/constitution.md +18 -18
  18. package/framework/context/protocols.md +1 -1
  19. package/framework/context/quality.md +1 -1
  20. package/framework/context/root.md +4 -4
  21. package/framework/data/entity-registry.yaml +1 -1
  22. package/framework/domains/agents/orchestrator.yaml +2 -2
  23. package/framework/domains/constitution.yaml +1 -1
  24. package/framework/hooks/advance-trigger.js +4 -6
  25. package/framework/hooks/git-push-authority.js +45 -37
  26. package/framework/hooks/mode-governance.js +149 -40
  27. package/framework/hooks/model-governance.js +13 -20
  28. package/framework/hooks/prism-engine.js +74 -92
  29. package/framework/hooks/reasoning-escalator.js +23 -40
  30. package/framework/hooks/session-digest.js +12 -13
  31. package/framework/hooks/session-reader.js +224 -0
  32. package/framework/hooks/session-writer.js +195 -0
  33. package/framework/hooks/team-quality-gate.js +34 -24
  34. package/framework/i18n/en.yaml +2 -2
  35. package/framework/i18n/es.yaml +2 -2
  36. package/framework/i18n/fr.yaml +2 -2
  37. package/framework/i18n/pt.yaml +2 -2
  38. package/framework/intelligence/context-engine.md +4 -4
  39. package/framework/intelligence/memory-layer.md +1 -1
  40. package/framework/manifest.json +129 -119
  41. package/framework/manifest.sig +1 -1
  42. package/framework/orchestrator/chati-router.js +252 -25
  43. package/framework/orchestrator/chati.md +238 -70
  44. package/framework/schemas/session.schema.json +1 -1
  45. package/framework/tasks/orchestrator-deviation.md +1 -1
  46. package/framework/tasks/orchestrator-escalate.md +1 -1
  47. package/framework/tasks/orchestrator-handoff.md +6 -6
  48. package/framework/tasks/orchestrator-health.md +5 -9
  49. package/framework/tasks/orchestrator-mode-switch.md +3 -7
  50. package/framework/tasks/orchestrator-resume.md +10 -14
  51. package/framework/tasks/orchestrator-route.md +3 -3
  52. package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
  53. package/framework/tasks/orchestrator-status.md +9 -9
  54. package/framework/tasks/orchestrator-suggest-mode.md +1 -1
  55. package/framework/tasks/qa-impl-consolidate.md +2 -2
  56. package/framework/tasks/qa-impl-performance-test.md +4 -4
  57. package/framework/tasks/qa-impl-regression-check.md +4 -4
  58. package/framework/tasks/qa-impl-sast-scan.md +1 -1
  59. package/framework/tasks/qa-impl-test-execute.md +1 -1
  60. package/framework/tasks/qa-impl-verdict.md +2 -2
  61. package/framework/tasks/qa-planning-consolidate.md +3 -3
  62. package/framework/tasks/qa-planning-coverage-plan.md +2 -2
  63. package/framework/tasks/qa-planning-gate-define.md +4 -4
  64. package/framework/tasks/qa-planning-risk-matrix.md +4 -4
  65. package/framework/tasks/qa-planning-test-strategy.md +2 -2
  66. package/node_modules/@chati/browser-capability/src/index.js +12 -2
  67. package/node_modules/@chati/planning/src/index.js +24 -4
  68. package/node_modules/@chati/provider-registry/src/index.js +11 -0
  69. package/node_modules/@chati/rail/src/index.js +1967 -83
  70. package/node_modules/@chati/release-lane/README.md +12 -8
  71. package/node_modules/@chati/release-lane/package.json +1 -1
  72. package/node_modules/@chati/release-lane/src/index.js +1426 -58
  73. package/node_modules/@chati/review-council/src/index.js +63 -0
  74. package/node_modules/@chati/tracking-clickup/README.md +13 -0
  75. package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
  76. package/package-artifact-manifest.json +1 -0
  77. package/package-artifact-manifest.sig +1 -0
  78. package/package.json +16 -7
  79. package/scripts/verify-real-harness-e2e.js +1581 -0
  80. package/src/config/framework-adapter.js +4 -4
  81. package/src/installer/core.js +148 -53
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +65 -20
  85. package/src/installer-v2/catalog-client.js +531 -23
  86. package/src/installer-v2/index.js +91 -34
  87. package/src/installer-v2/installation-authority.js +327 -0
  88. package/src/installer-v2/provider-executable.js +247 -0
  89. package/src/installer-v2/wizard-installation.js +1 -1
  90. package/src/orchestrator/browser-runtime.js +44 -13
  91. package/src/orchestrator/cli.js +1545 -174
  92. package/src/orchestrator/clickup-projection.js +43 -1
  93. package/src/orchestrator/clickup-runtime.js +355 -39
  94. package/src/orchestrator/doctor.js +16 -3
  95. package/src/orchestrator/index.js +16 -0
  96. package/src/orchestrator/planning-runtime.js +20 -2
  97. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  98. package/src/orchestrator/rail-evidence-authority.js +147 -0
  99. package/src/orchestrator/rail-execution-evidence.js +45 -0
  100. package/src/orchestrator/rail-runtime.js +836 -56
  101. package/src/orchestrator/release-runtime.js +65 -6
  102. package/src/orchestrator/review-runtime.js +186 -41
  103. package/src/orchestrator/runtime-installation-v2.js +236 -20
  104. package/src/orchestrator/session-manager.js +1167 -53
  105. package/src/terminal/adapters/claude-adapter.js +3 -1
  106. package/src/terminal/adapters/codex-adapter.js +2 -0
  107. package/src/terminal/adapters/grok-adapter.js +7 -4
  108. package/src/terminal/handoff-parser.js +19 -1
  109. package/src/terminal/prompt-builder.js +9 -1
  110. package/src/terminal/provider-preflight.js +36 -3
  111. package/src/terminal/rail-execution-worktree.js +213 -0
  112. package/src/terminal/rail-prompts.js +169 -0
  113. package/src/terminal/rail-readonly-workspace.js +324 -0
  114. package/src/terminal/run-agent.js +384 -24
  115. package/src/terminal/run-parallel.js +5 -0
  116. package/src/terminal/run-rail-adjudication.js +323 -0
  117. package/src/terminal/run-rail-review.js +291 -0
  118. package/src/terminal/run-rail-rework.js +325 -0
  119. package/src/terminal/run-rail-task.js +380 -0
  120. package/src/terminal/run-team.js +5 -0
  121. package/src/terminal/spawner.js +1225 -77
  122. package/src/wizard/index.js +3 -2
@@ -0,0 +1,247 @@
1
+ import { createHash } from 'node:crypto';
2
+ import {
3
+ closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync,
4
+ } from 'node:fs';
5
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+
7
+ const MAX_PROVIDER_EXECUTABLE_BYTES = 1024 * 1024 * 1024;
8
+ const PROVIDER_EXECUTABLE_DIGEST = /^[a-f0-9]{64}$/;
9
+ const CODEX_SUPPORTING_EXECUTABLE = 'codex-code-mode-host';
10
+ const CODEX_NATIVE_TARGETS = Object.freeze({
11
+ 'linux/x64': Object.freeze(['@openai/codex-linux-x64', 'x86_64-unknown-linux-musl']),
12
+ 'linux/arm64': Object.freeze(['@openai/codex-linux-arm64', 'aarch64-unknown-linux-musl']),
13
+ 'darwin/x64': Object.freeze(['@openai/codex-darwin-x64', 'x86_64-apple-darwin']),
14
+ 'darwin/arm64': Object.freeze(['@openai/codex-darwin-arm64', 'aarch64-apple-darwin']),
15
+ });
16
+
17
+ function fail(code, message) {
18
+ throw Object.assign(new Error(message), { code });
19
+ }
20
+
21
+ function snapshot(stat) {
22
+ return Object.freeze({
23
+ dev: stat.dev, ino: stat.ino, mode: stat.mode, uid: stat.uid, gid: stat.gid,
24
+ nlink: stat.nlink, size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs,
25
+ });
26
+ }
27
+
28
+ function sameSnapshot(left, right) {
29
+ return Object.keys(left).every((key) => left[key] === right[key]);
30
+ }
31
+
32
+ function assertExecutableStat(stat) {
33
+ if (!stat.isFile() || stat.nlink < 1 || stat.size < 1 || stat.size > MAX_PROVIDER_EXECUTABLE_BYTES
34
+ || (stat.mode & 0o111) === 0) {
35
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'provider executable binding must identify one bounded executable regular file');
36
+ }
37
+ }
38
+
39
+ export function digestProviderExecutable(descriptor, expectedSnapshot = null) {
40
+ const before = snapshot(fstatSync(descriptor));
41
+ assertExecutableStat(fstatSync(descriptor));
42
+ const hash = createHash('sha256');
43
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
44
+ let position = 0;
45
+ while (position < before.size) {
46
+ const bytesRead = readSync(descriptor, buffer, 0, Math.min(buffer.length, before.size - position), position);
47
+ if (bytesRead === 0) fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable changed while it was being hashed');
48
+ hash.update(buffer.subarray(0, bytesRead));
49
+ position += bytesRead;
50
+ }
51
+ const after = snapshot(fstatSync(descriptor));
52
+ if (!sameSnapshot(before, after) || (expectedSnapshot && !sameSnapshot(after, expectedSnapshot))) {
53
+ fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable changed during trust validation');
54
+ }
55
+ return Object.freeze({ sha256: hash.digest('hex'), snapshot: after });
56
+ }
57
+
58
+ function resolveFromPath(command, pathEnv) {
59
+ for (const entry of String(pathEnv || '').split(delimiter).filter(Boolean)) {
60
+ const candidate = resolve(entry, command);
61
+ try {
62
+ const stat = lstatSync(candidate);
63
+ if ((stat.isFile() || stat.isSymbolicLink()) && (stat.mode & 0o111) !== 0) return realpathSync(candidate);
64
+ } catch { /* continue through the explicit search path */ }
65
+ }
66
+ return fail('PROVIDER_EXECUTABLE_NOT_FOUND', `selected provider executable ${command} was not found`);
67
+ }
68
+
69
+ function codexNativeExecutable(wrapperPath, { platform = process.platform, arch = process.arch } = {}) {
70
+ const target = CODEX_NATIVE_TARGETS[`${platform}/${arch}`];
71
+ if (!target) fail('PROVIDER_EXECUTABLE_UNSUPPORTED', `Codex native executable binding is unsupported on ${platform}/${arch}`);
72
+ const [packageName, triple] = target;
73
+ const packageRoot = dirname(dirname(wrapperPath));
74
+ const nativePackageDirectory = basename(packageName);
75
+ const candidates = [
76
+ join(packageRoot, 'node_modules', packageName, 'vendor', triple, 'bin', 'codex'),
77
+ join(dirname(packageRoot), nativePackageDirectory, 'vendor', triple, 'bin', 'codex'),
78
+ join(packageRoot, 'vendor', triple, 'bin', 'codex'),
79
+ ];
80
+ for (const candidate of candidates) {
81
+ try {
82
+ const resolved = realpathSync(candidate);
83
+ assertExecutableStat(lstatSync(resolved));
84
+ return resolved;
85
+ } catch { /* try the next supported package layout */ }
86
+ }
87
+ return fail('PROVIDER_EXECUTABLE_NOT_FOUND', 'Codex wrapper does not resolve to its installed native executable');
88
+ }
89
+
90
+ function effectiveProviderExecutable(harnessId, resolvedCommand, options) {
91
+ if (harnessId !== 'codex') return resolvedCommand;
92
+ let descriptor;
93
+ let prefix;
94
+ try {
95
+ descriptor = openSync(resolvedCommand, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
96
+ const buffer = Buffer.alloc(64);
97
+ const bytes = readSync(descriptor, buffer, 0, buffer.length, 0);
98
+ prefix = buffer.subarray(0, bytes).toString('utf8');
99
+ } finally {
100
+ if (descriptor !== undefined) closeSync(descriptor);
101
+ }
102
+ return prefix.startsWith('#!') ? codexNativeExecutable(resolvedCommand, options) : resolvedCommand;
103
+ }
104
+
105
+ function digestExecutablePath(executablePath) {
106
+ let descriptor;
107
+ try {
108
+ const pathStat = lstatSync(executablePath);
109
+ if (pathStat.isSymbolicLink()) fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'provider executable binding may not end in a symlink');
110
+ descriptor = openSync(executablePath, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
111
+ const pinned = fstatSync(descriptor);
112
+ if (pathStat.dev !== pinned.dev || pathStat.ino !== pinned.ino) {
113
+ fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable changed before descriptor pinning');
114
+ }
115
+ return digestProviderExecutable(descriptor);
116
+ } finally {
117
+ if (descriptor !== undefined) closeSync(descriptor);
118
+ }
119
+ }
120
+
121
+ function supportingExecutablesFor(harnessId, executablePath) {
122
+ if (harnessId !== 'codex') return Object.freeze([]);
123
+ const supportPath = join(dirname(executablePath), CODEX_SUPPORTING_EXECUTABLE);
124
+ const executable_path = realpathSync(supportPath);
125
+ if (dirname(executable_path) !== dirname(executablePath) || basename(executable_path) !== CODEX_SUPPORTING_EXECUTABLE) {
126
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'Codex supporting executable must be its native code-mode host sibling');
127
+ }
128
+ const digest = digestExecutablePath(executable_path);
129
+ return Object.freeze([Object.freeze({ executable_path, executable_sha256: digest.sha256 })]);
130
+ }
131
+
132
+ export function resolveProviderExecutableBinding(harnessId, {
133
+ pathEnv = process.env.PATH,
134
+ platform = process.platform,
135
+ arch = process.arch,
136
+ } = {}) {
137
+ if (!['claude', 'codex', 'grok'].includes(harnessId)) {
138
+ fail('PROVIDER_EXECUTABLE_UNSUPPORTED', `provider executable binding is unsupported for ${harnessId}`);
139
+ }
140
+ const executable_path = effectiveProviderExecutable(
141
+ harnessId,
142
+ resolveFromPath(harnessId, pathEnv),
143
+ { platform, arch },
144
+ );
145
+ const digest = digestExecutablePath(executable_path);
146
+ return Object.freeze({
147
+ executable_path,
148
+ executable_sha256: digest.sha256,
149
+ supporting_executables: supportingExecutablesFor(harnessId, executable_path),
150
+ });
151
+ }
152
+
153
+ function validateExecutableReference(reference, label, { exact = false } = {}) {
154
+ if (!reference || typeof reference !== 'object' || Array.isArray(reference)
155
+ || typeof reference.executable_path !== 'string' || !isAbsolute(reference.executable_path)
156
+ || resolve(reference.executable_path) !== reference.executable_path
157
+ || typeof reference.executable_sha256 !== 'string'
158
+ || !PROVIDER_EXECUTABLE_DIGEST.test(reference.executable_sha256)) {
159
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', `${label} must contain a canonical absolute path and SHA-256 digest`);
160
+ }
161
+ if (exact && Object.keys(reference).some((key) => key !== 'executable_path' && key !== 'executable_sha256')) {
162
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', `${label} contains unsupported fields`);
163
+ }
164
+ return Object.freeze({
165
+ executable_path: reference.executable_path,
166
+ executable_sha256: reference.executable_sha256,
167
+ });
168
+ }
169
+
170
+ export function validateProviderExecutableBinding(binding, { harnessId, requireCodexSupportingExecutable = false } = {}) {
171
+ const primary = validateExecutableReference(binding, `${harnessId || 'provider'} executable binding`);
172
+ const rawSupporting = binding.supporting_executables === undefined ? [] : binding.supporting_executables;
173
+ if (!Array.isArray(rawSupporting)) {
174
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', `${harnessId || 'provider'} supporting executables must be an array`);
175
+ }
176
+ const supporting_executables = rawSupporting.map((reference) => validateExecutableReference(
177
+ reference, `${harnessId || 'provider'} supporting executable`, { exact: true },
178
+ ));
179
+ if (harnessId !== 'codex' && supporting_executables.length > 0) {
180
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'only Codex may declare supporting executables');
181
+ }
182
+ if (harnessId === 'codex' && ((requireCodexSupportingExecutable && supporting_executables.length !== 1)
183
+ || (supporting_executables.length > 0 && (
184
+ supporting_executables.length !== 1
185
+ || basename(supporting_executables[0].executable_path) !== CODEX_SUPPORTING_EXECUTABLE
186
+ || dirname(supporting_executables[0].executable_path) !== dirname(primary.executable_path)
187
+ )))) {
188
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'Codex supporting executable must be its native code-mode host sibling');
189
+ }
190
+ return Object.freeze({
191
+ ...primary,
192
+ supporting_executables: Object.freeze(supporting_executables),
193
+ });
194
+ }
195
+
196
+ function assertOutsideProject(executablePath, projectDir) {
197
+ if (!projectDir) return;
198
+ const relativePath = relative(resolve(projectDir), executablePath);
199
+ if (relativePath === '' || (relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath))) {
200
+ fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'provider executable must be installed outside the controlled project tree');
201
+ }
202
+ }
203
+
204
+ function openPinnedExecutable(reference, { projectDir } = {}) {
205
+ assertOutsideProject(reference.executable_path, projectDir);
206
+ let descriptor;
207
+ try {
208
+ const pathStat = lstatSync(reference.executable_path);
209
+ if (pathStat.isSymbolicLink()) fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'provider executable binding may not end in a symlink');
210
+ descriptor = openSync(reference.executable_path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
211
+ const pinned = fstatSync(descriptor);
212
+ if (pathStat.dev !== pinned.dev || pathStat.ino !== pinned.ino) {
213
+ fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable changed before descriptor pinning');
214
+ }
215
+ const executable = digestProviderExecutable(descriptor);
216
+ if (executable.sha256 !== reference.executable_sha256) {
217
+ fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable digest differs from the sealed installation binding');
218
+ }
219
+ return Object.freeze({ descriptor, binding: reference, executable });
220
+ } catch (error) {
221
+ if (descriptor !== undefined) closeSync(descriptor);
222
+ if (error?.code?.startsWith('PROVIDER_EXECUTABLE_')) throw error;
223
+ return fail('PROVIDER_EXECUTABLE_BINDING_INVALID', `provider executable binding cannot be opened safely: ${error.message}`);
224
+ }
225
+ }
226
+
227
+ export function openPinnedProviderExecutable(binding, { harnessId, projectDir } = {}) {
228
+ const validated = validateProviderExecutableBinding(binding, { harnessId });
229
+ return openPinnedExecutable(validated, { projectDir });
230
+ }
231
+
232
+ export function openPinnedProviderSupportingExecutables(binding, { harnessId, projectDir } = {}) {
233
+ const validated = validateProviderExecutableBinding(binding, { harnessId });
234
+ return Object.freeze(validated.supporting_executables.map((reference) => openPinnedExecutable(reference, { projectDir })));
235
+ }
236
+
237
+ export function bindProviderExecutables(input, options = {}) {
238
+ if (!input?.installation?.enabled_providers) fail('INVALID_INSTALLATION', 'installation input is required');
239
+ const enabled_providers = input.installation.enabled_providers.map((binding) => ({
240
+ ...binding,
241
+ ...resolveProviderExecutableBinding(binding.harness_id, options),
242
+ }));
243
+ return Object.freeze({
244
+ ...input,
245
+ installation: Object.freeze({ ...input.installation, enabled_providers: Object.freeze(enabled_providers) }),
246
+ });
247
+ }
@@ -109,7 +109,7 @@ export function buildWizardV2InstallationInput({
109
109
  return Object.freeze({
110
110
  project_id: `project-${projectSlug}`,
111
111
  installation,
112
- capability_snapshot: { snapshot_id: snapshotId, expires_at: expiresAt, models },
112
+ capability_snapshot: { snapshot_id: snapshotId, source_catalog_ref: sourceCatalogId, expires_at: expiresAt, models },
113
113
  });
114
114
  }
115
115
 
@@ -1,25 +1,56 @@
1
- import { assertBrowserCapabilityForTask, BrowserCapability } from '@chati/browser-capability';
1
+ import { canonicalize, sha256 } from '@chati/core';
2
+ import { assertBrowserCapabilityForTask, validateBrowserLaunchRequest } from '@chati/browser-capability';
2
3
 
3
4
  /**
4
- * G3 browser boundary. This runtime has no reference to Playwright, Chrome,
5
- * MCP, credentials or a shared profile. It can only validate a task policy and
6
- * use the internal fake adapter supplied by BrowserCapability.
5
+ * Production browser boundary. It authorizes a policy-bound delegation to the
6
+ * selected harness but never claims that a browser was launched. The harness
7
+ * must later produce real, redacted browser evidence for review and completion.
7
8
  */
8
9
  export function createProjectBrowserSession({ task, request } = {}) {
9
10
  const policy = assertBrowserCapabilityForTask(task);
10
- if (!policy) return Object.freeze({ state: 'not-required', manifest: null, capability: null });
11
- const capability = new BrowserCapability({});
12
- const manifest = capability.preflight({ ...request, policy });
13
- return Object.freeze({ state: 'preflighted', policy, manifest, capability });
11
+ if (!policy) return Object.freeze({ state: 'not-required', manifest: null, policy: null });
12
+ const validatedRequest = validateBrowserLaunchRequest(request, policy);
13
+ const core = canonicalize({
14
+ schema_version: 1,
15
+ manifest_type: 'BrowserDelegationManifest',
16
+ browser_session_id: validatedRequest.browser_session_id,
17
+ task_id: validatedRequest.task_id,
18
+ policy_ref: policy.policy_id,
19
+ mode: policy.mode,
20
+ tool: policy.tool,
21
+ allowed_domain: validatedRequest.hostname,
22
+ profile_isolation: policy.profile_isolation,
23
+ classification: validatedRequest.classification,
24
+ evidence_class: 'delegation-only',
25
+ artifact_policy: policy.artifact_policy,
26
+ action_confirmation: policy.action_confirmation,
27
+ ...(validatedRequest.tenant_ref ? { tenant_ref: validatedRequest.tenant_ref } : {}),
28
+ ...(validatedRequest.action_confirmation_ref ? { action_confirmation_ref: validatedRequest.action_confirmation_ref } : {}),
29
+ delegation: { adapter: 'selected-harness-browser-tool', status: 'authorized', evidence_status: 'required' },
30
+ });
31
+ const manifest = Object.freeze(canonicalize({ ...core, manifest_hash: sha256(core) }));
32
+ return Object.freeze({ state: 'authorized', policy, request: validatedRequest, manifest });
14
33
  }
15
34
 
16
- /** Only the same local session may advance its fake adapter past preflight. */
35
+ /** Marks the validated request as delegated, without fabricating launch I/O. */
17
36
  export function launchProjectBrowserSession({ session, request } = {}) {
18
- if (!session || session.state !== 'preflighted' || !session.capability || !session.policy) {
19
- const error = new Error('browser launch requires a preflighted project-local browser session');
37
+ if (!session || session.state !== 'authorized' || !session.policy || !session.request) {
38
+ const error = new Error('browser delegation requires an authorized project-local browser session');
20
39
  error.code = 'BROWSER_PREFLIGHT_REQUIRED';
21
40
  throw error;
22
41
  }
23
- const manifest = session.capability.launch({ ...request, policy: session.policy });
24
- return Object.freeze({ state: 'launched', manifest, fake_launches: session.capability.fakeLaunches, capability: session.capability, policy: session.policy });
42
+ const validated = validateBrowserLaunchRequest(request, session.policy);
43
+ if (sha256(validated) !== sha256(session.request)) {
44
+ const error = new Error('browser delegation request changed after authorization');
45
+ error.code = 'BROWSER_REQUEST_CHANGED';
46
+ throw error;
47
+ }
48
+ const priorManifest = { ...session.manifest };
49
+ delete priorManifest.manifest_hash;
50
+ const core = canonicalize({
51
+ ...priorManifest,
52
+ delegation: { adapter: 'selected-harness-browser-tool', status: 'delegated', evidence_status: 'required' },
53
+ });
54
+ const manifest = Object.freeze(canonicalize({ ...core, manifest_hash: sha256(core) }));
55
+ return Object.freeze({ state: 'delegated', manifest, policy: session.policy });
25
56
  }