chati-dev 4.5.28 → 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.
- package/bin/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +117 -12
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
2
3
|
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
buildSpawnCommand, getTerminalCompletionAuthorityReceipt, getTerminalLaunchAuthorityReceipt,
|
|
9
10
|
isTransientFailure, killTerminal, spawnTerminal, waitForTerminal,
|
|
10
11
|
} from './spawner.js';
|
|
12
|
+
import { waitForObservedRailProcess } from './rail-process-observer.js';
|
|
11
13
|
import { checkProviderReadiness } from './provider-preflight.js';
|
|
12
14
|
import { getRateLimiter } from './rate-limiter.js';
|
|
13
15
|
import { startClaimHeartbeat } from './run-rail-task.js';
|
|
@@ -18,7 +20,8 @@ import {
|
|
|
18
20
|
listRailReviewEvidence, recordRailReview, recordRailReviewDispatch, resolveReviewGitSubject,
|
|
19
21
|
} from '../orchestrator/review-runtime.js';
|
|
20
22
|
import {
|
|
21
|
-
|
|
23
|
+
assertRuntimeTaskReviewBindingV2, loadRuntimeInstallationV2,
|
|
24
|
+
resolveRuntimeExecutableBindingV2, resolveRuntimeRailReviewerV2,
|
|
22
25
|
} from '../orchestrator/runtime-installation-v2.js';
|
|
23
26
|
import { buildRailReviewPrompt } from './rail-prompts.js';
|
|
24
27
|
import {
|
|
@@ -58,7 +61,11 @@ function parseReviewPayload(text) {
|
|
|
58
61
|
catch (error) { return fail('RAIL_REVIEW_OUTPUT_INVALID', `reviewer returned invalid JSON: ${error.message}`); }
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
export async function executeRailReview({
|
|
64
|
+
export async function executeRailReview(options = {}) {
|
|
65
|
+
return withUpgradeMaintenance(options?.projectDir, () => executeRailReviewUnderMaintenance(options));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function executeRailReviewUnderMaintenance({
|
|
62
69
|
projectDir,
|
|
63
70
|
handoff_id,
|
|
64
71
|
attempt_id,
|
|
@@ -74,7 +81,11 @@ export async function executeRailReview({
|
|
|
74
81
|
provider_id: context.task.execution_binding.provider_id,
|
|
75
82
|
model_id: context.task.execution_binding.model_id,
|
|
76
83
|
};
|
|
77
|
-
|
|
84
|
+
// A v2 execution-design handoff seals its reviewer during planning. Legacy
|
|
85
|
+
// handoffs retain the prior catalog selection path for compatibility only.
|
|
86
|
+
const selected = context.task.review_binding
|
|
87
|
+
? assertRuntimeTaskReviewBindingV2({ artifact, review_binding: context.task.review_binding, builder_identity })
|
|
88
|
+
: resolveRuntimeRailReviewerV2({ artifact, builder_identity });
|
|
78
89
|
const providerExecutableBinding = resolveRuntimeExecutableBindingV2({
|
|
79
90
|
artifact, provider_id: selected.provider_id, harness_id: selected.harness_id,
|
|
80
91
|
});
|
|
@@ -172,7 +183,7 @@ export async function executeRailReview({
|
|
|
172
183
|
({ handle, invocation } = await spawnReviewAttempt(0, () => {
|
|
173
184
|
if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'review lost its RAIL claim heartbeat');
|
|
174
185
|
}));
|
|
175
|
-
await
|
|
186
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id });
|
|
176
187
|
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL review' });
|
|
177
188
|
while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
|
|
178
189
|
if (heartbeat.evidence().failure) break;
|
|
@@ -182,7 +193,7 @@ export async function executeRailReview({
|
|
|
182
193
|
({ handle, invocation } = await spawnReviewAttempt(retries, () => {
|
|
183
194
|
if (heartbeat.evidence().failure) fail('RAIL_LEASE_HEARTBEAT_FAILED', 'review lost its RAIL claim heartbeat');
|
|
184
195
|
}));
|
|
185
|
-
await
|
|
196
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id });
|
|
186
197
|
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace: reviewWorkspace, label: 'RAIL review retry' });
|
|
187
198
|
}
|
|
188
199
|
} finally { heartbeat.stop(); }
|
|
@@ -217,13 +228,7 @@ export async function executeRailReview({
|
|
|
217
228
|
invocation,
|
|
218
229
|
result_commit_ref: reviewSubject.result_commit_ref,
|
|
219
230
|
});
|
|
220
|
-
const next =
|
|
221
|
-
accept: 'rail_completion_required',
|
|
222
|
-
'normal-rework': 'rail_rework_required',
|
|
223
|
-
AI_ADJUDICATION: 'rail_adjudication_required',
|
|
224
|
-
'required-human-decision': 'human_authority_required',
|
|
225
|
-
blocked: 'rail_blocked',
|
|
226
|
-
}[recorded.decision.decision] || 'rail_blocked';
|
|
231
|
+
const next = reviewNextRequiredAction(recorded.decision.decision);
|
|
227
232
|
return Object.freeze({
|
|
228
233
|
ok: recorded.decision.decision === 'accept',
|
|
229
234
|
action: 'rail_review_recorded',
|
|
@@ -239,12 +244,22 @@ export async function executeRailReview({
|
|
|
239
244
|
next_required_action: next,
|
|
240
245
|
});
|
|
241
246
|
} finally {
|
|
242
|
-
|
|
247
|
+
// Darwin's valid group completion permits this readonly invocation to
|
|
248
|
+
// relinquish its claim, but does not prove that every detached reader died.
|
|
249
|
+
const retainWorkspace = process.platform === 'darwin';
|
|
250
|
+
let cleanupProven = !handle && !retainWorkspace;
|
|
243
251
|
try {
|
|
244
|
-
if (handle) {
|
|
252
|
+
if (handle) {
|
|
253
|
+
await killTerminal(handle);
|
|
254
|
+
if (retainWorkspace) {
|
|
255
|
+
await waitForTerminal(handle);
|
|
256
|
+
getTerminalCompletionAuthorityReceipt(handle);
|
|
257
|
+
}
|
|
258
|
+
cleanupProven = true;
|
|
259
|
+
}
|
|
245
260
|
} finally {
|
|
246
261
|
try {
|
|
247
|
-
if (cleanupProven && reviewWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
|
|
262
|
+
if (cleanupProven && reviewWorkspace && !retainWorkspace) cleanupRailReadOnlyCommitWorkspace(reviewWorkspace);
|
|
248
263
|
} finally {
|
|
249
264
|
if (cleanupProven) {
|
|
250
265
|
relinquishRailTask({
|
|
@@ -288,4 +303,10 @@ async function main() {
|
|
|
288
303
|
}
|
|
289
304
|
}
|
|
290
305
|
|
|
306
|
+
export function reviewNextRequiredAction(decision) {
|
|
307
|
+
return { accept: 'rail_completion_required', 'normal-rework': 'rail_rework_required',
|
|
308
|
+
AI_ADJUDICATION: 'rail_adjudication_required', 'required-human-decision': 'human_authority_required',
|
|
309
|
+
blocked: 'rail_blocked' }[decision] || 'rail_blocked';
|
|
310
|
+
}
|
|
311
|
+
|
|
291
312
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) await main();
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
2
3
|
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
6
7
|
import {
|
|
7
8
|
buildSpawnCommand, getTerminalCompletionAuthorityReceipt,
|
|
8
|
-
|
|
9
|
+
isTransientFailure, killTerminal, spawnTerminal,
|
|
9
10
|
} from './spawner.js';
|
|
11
|
+
import { waitForObservedRailProcess } from './rail-process-observer.js';
|
|
10
12
|
import { checkProviderReadiness } from './provider-preflight.js';
|
|
11
13
|
import { getRateLimiter } from './rate-limiter.js';
|
|
12
14
|
import { startClaimHeartbeat } from './run-rail-task.js';
|
|
@@ -70,10 +72,17 @@ function latestReworkAuthorization(context) {
|
|
|
70
72
|
return fail('RAIL_REWORK_NOT_AUTHORIZED', 'the latest canonical decision does not authorize bounded rework');
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export async function executeRailRework({
|
|
75
|
+
export async function executeRailRework(options = {}) {
|
|
76
|
+
return withUpgradeMaintenance(options?.projectDir, () => executeRailReworkUnderMaintenance(options));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function executeRailReworkUnderMaintenance({
|
|
74
80
|
projectDir,
|
|
75
81
|
handoff_id,
|
|
76
82
|
attempt_id,
|
|
83
|
+
operator_id,
|
|
84
|
+
git_dir = projectDir,
|
|
85
|
+
expected_attempt_event_id,
|
|
77
86
|
timeout = 30 * 60 * 1000,
|
|
78
87
|
max_retries = 2,
|
|
79
88
|
lease_ttl_ms = 5 * 60 * 1000,
|
|
@@ -111,7 +120,7 @@ export async function executeRailRework({
|
|
|
111
120
|
if (!readiness.ready) fail(readiness.code || 'PROVIDER_NOT_READY', readiness.detail || `${binding.harness_id} is not ready`);
|
|
112
121
|
const base_commit_ref = executionHistory.filter((entry) => typeof entry.result_commit_ref === 'string').at(-1)?.result_commit_ref;
|
|
113
122
|
if (!base_commit_ref) fail('RAIL_REWORK_BASE_COMMIT_REQUIRED', 'latest immutable execution evidence has no result commit');
|
|
114
|
-
const ownership = acquireRailTaskSupervisor({ projectDir, handoff_id, attempt_id, lease_ttl_ms });
|
|
123
|
+
const ownership = acquireRailTaskSupervisor({ projectDir, handoff_id, attempt_id, operator_id, git_dir, lease_ttl_ms, expected_attempt_event_id });
|
|
115
124
|
context = ownership.context;
|
|
116
125
|
const renewed = ownership.renewed;
|
|
117
126
|
const reservation_id = `rework-reservation-${randomUUID()}`;
|
|
@@ -166,32 +175,34 @@ export async function executeRailRework({
|
|
|
166
175
|
authority_kind: 'rail_execution', invocation_id: invocation.invocation_id,
|
|
167
176
|
launch_nonce: invocation.launch_nonce, provider_id: invocation.provider_id,
|
|
168
177
|
},
|
|
169
|
-
beforeAuthoritySpawn: () => {
|
|
178
|
+
beforeAuthoritySpawn: ({ admission_receipt }) => {
|
|
170
179
|
reserveRailReworkLaunch({
|
|
171
180
|
projectDir, handoff_id, attempt_id,
|
|
172
181
|
source_decision_ref: authorization.source_decision_ref,
|
|
173
182
|
reservation_id,
|
|
174
183
|
invocation,
|
|
184
|
+
admission_receipt,
|
|
175
185
|
});
|
|
176
186
|
// From the final pre-spawn durable launch intent onward, absence of
|
|
177
187
|
// canonical completion evidence remains unresolved after a crash.
|
|
178
188
|
dispatchAwaitingEvidence = true;
|
|
179
189
|
},
|
|
190
|
+
afterAuthoritySpawn: (launch_receipt, pendingHandle) => {
|
|
191
|
+
handle = pendingHandle;
|
|
192
|
+
return recordRailExecutionDispatch({
|
|
193
|
+
projectDir, handoff_id, attempt_id, invocation, launch_receipt,
|
|
194
|
+
prompt, run_kind: 'rework',
|
|
195
|
+
});
|
|
196
|
+
},
|
|
180
197
|
};
|
|
181
198
|
const built = buildSpawnCommand(config);
|
|
182
199
|
if (built.actualProvider !== config.provider || built.actualModel !== config.model
|
|
183
200
|
|| built.actualReasoningConfiguration !== config.reasoningConfiguration) {
|
|
184
201
|
fail('RAIL_REWORK_ROUTE_MISMATCH', 'terminal adapter changed the sealed rework binding');
|
|
185
202
|
}
|
|
186
|
-
handle = spawnTerminal(config);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
projectDir, handoff_id, attempt_id, invocation,
|
|
190
|
-
launch_receipt: getTerminalLaunchAuthorityReceipt(handle),
|
|
191
|
-
prompt, run_kind: 'rework',
|
|
192
|
-
});
|
|
193
|
-
} catch (error) {
|
|
194
|
-
await killTerminal(handle);
|
|
203
|
+
try { handle = spawnTerminal(config); }
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (handle) await killTerminal(handle);
|
|
195
206
|
throw error;
|
|
196
207
|
}
|
|
197
208
|
return handle;
|
|
@@ -214,7 +225,7 @@ export async function executeRailRework({
|
|
|
214
225
|
failedAttemptRefs.push(persisted.evidence_ref);
|
|
215
226
|
};
|
|
216
227
|
try {
|
|
217
|
-
await
|
|
228
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id });
|
|
218
229
|
while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
|
|
219
230
|
if (heartbeat.evidence().failure) break;
|
|
220
231
|
recordFailedAttempt();
|
|
@@ -224,7 +235,7 @@ export async function executeRailRework({
|
|
|
224
235
|
await getRateLimiter(binding.harness_id).waitForSlot();
|
|
225
236
|
if (heartbeat.evidence().failure) break;
|
|
226
237
|
await launch();
|
|
227
|
-
await
|
|
238
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id });
|
|
228
239
|
}
|
|
229
240
|
} finally { heartbeat.stop(); }
|
|
230
241
|
const heartbeatEvidence = heartbeat.evidence();
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
2
3
|
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
7
|
+
import { attemptExecutionBase } from '@chati/rail';
|
|
6
8
|
import {
|
|
7
9
|
buildSpawnCommand, getTerminalCompletionAuthorityReceipt,
|
|
8
|
-
getTerminalLaunchAuthorityReceipt, isTransientFailure, killTerminal, spawnTerminal,
|
|
10
|
+
getTerminalLaunchAuthorityReceipt, isTransientFailure, killTerminal, spawnTerminal,
|
|
9
11
|
} from './spawner.js';
|
|
12
|
+
import { waitForObservedRailProcess } from './rail-process-observer.js';
|
|
10
13
|
import { checkProviderReadiness } from './provider-preflight.js';
|
|
11
14
|
import { getRateLimiter } from './rate-limiter.js';
|
|
12
15
|
import {
|
|
@@ -139,7 +142,11 @@ export function startClaimHeartbeat({ projectDir, handoff_id, attempt_id, claim_
|
|
|
139
142
|
* dispatchReadyRailTask and starts the exact selected CLI binding from inside
|
|
140
143
|
* the injected adapter. Retries reuse that same binding and never downgrade.
|
|
141
144
|
*/
|
|
142
|
-
export async function executeReadyRailTask({
|
|
145
|
+
export async function executeReadyRailTask(options = {}) {
|
|
146
|
+
return withUpgradeMaintenance(options?.projectDir, () => executeReadyRailTaskUnderMaintenance(options));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function executeReadyRailTaskUnderMaintenance({
|
|
143
150
|
projectDir,
|
|
144
151
|
handoff_id,
|
|
145
152
|
task_id,
|
|
@@ -149,11 +156,12 @@ export async function executeReadyRailTask({
|
|
|
149
156
|
git_dir = projectDir,
|
|
150
157
|
lease_ttl_ms,
|
|
151
158
|
recover_expired = false,
|
|
159
|
+
expected_attempt_event_id,
|
|
152
160
|
timeout = 30 * 60 * 1000,
|
|
153
161
|
max_retries = 2,
|
|
154
162
|
} = {}) {
|
|
155
163
|
if (!projectDir || !handoff_id || !task_id) fail('RAIL_DISPATCH_INPUT_REQUIRED', 'projectDir, handoff_id and task_id are required');
|
|
156
|
-
const {
|
|
164
|
+
const { task } = exactTask({ projectDir, handoff_id, task_id });
|
|
157
165
|
const binding = task.execution_binding;
|
|
158
166
|
const providerExecutableBinding = resolveRuntimeExecutableBindingV2({
|
|
159
167
|
artifact: loadRuntimeInstallationV2(projectDir),
|
|
@@ -169,13 +177,17 @@ export async function executeReadyRailTask({
|
|
|
169
177
|
let exactConfig = null;
|
|
170
178
|
const adapter = Object.freeze({
|
|
171
179
|
harness_id: binding.harness_id,
|
|
172
|
-
invoke({ invocation, prompt, reserve_launch }) {
|
|
180
|
+
invoke({ attempt, invocation, prompt, reserve_launch, record_dispatch }) {
|
|
173
181
|
exactConfig = Object.freeze({
|
|
174
182
|
...terminalConfig({
|
|
175
183
|
projectDir, task, invocation, prompt, timeout, providerExecutableBinding,
|
|
176
|
-
baseCommitRef:
|
|
184
|
+
baseCommitRef: attemptExecutionBase(attempt),
|
|
177
185
|
}),
|
|
178
186
|
beforeAuthoritySpawn: reserve_launch,
|
|
187
|
+
afterAuthoritySpawn: (receipt, pendingHandle) => {
|
|
188
|
+
handle = pendingHandle;
|
|
189
|
+
return record_dispatch(receipt);
|
|
190
|
+
},
|
|
179
191
|
});
|
|
180
192
|
assertExactTerminalRoute(exactConfig);
|
|
181
193
|
handle = spawnTerminal(exactConfig);
|
|
@@ -191,6 +203,7 @@ export async function executeReadyRailTask({
|
|
|
191
203
|
projectDir, handoff_id, task_id, attempt_id, operator_id,
|
|
192
204
|
adapter, browser_request, git_dir, lease_ttl_ms: effectiveLeaseTtlMs,
|
|
193
205
|
recover_expired,
|
|
206
|
+
expected_attempt_event_id,
|
|
194
207
|
});
|
|
195
208
|
} catch (error) {
|
|
196
209
|
if (handle) await killTerminal(handle);
|
|
@@ -230,13 +243,13 @@ export async function executeReadyRailTask({
|
|
|
230
243
|
projectDir, handoff_id, task_id, attempt_id,
|
|
231
244
|
invocation: dispatch.invocation,
|
|
232
245
|
execution: failedExecution,
|
|
233
|
-
base_commit_ref: dispatch.claim.attempt
|
|
246
|
+
base_commit_ref: attemptExecutionBase(dispatch.claim.attempt),
|
|
234
247
|
result_commit_ref: null,
|
|
235
248
|
});
|
|
236
249
|
failedAttemptRefs.push(persisted.evidence_ref);
|
|
237
250
|
};
|
|
238
251
|
try {
|
|
239
|
-
await
|
|
252
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id });
|
|
240
253
|
while (handle.exitCode !== 0 && retries < max_retries && isTransientFailure(handle.exitCode, [...handle.stdout, ...handle.stderr])) {
|
|
241
254
|
if (heartbeat.evidence().failure) break;
|
|
242
255
|
recordFailedAttempt();
|
|
@@ -253,7 +266,7 @@ export async function executeReadyRailTask({
|
|
|
253
266
|
if (handle) await killTerminal(handle);
|
|
254
267
|
throw error;
|
|
255
268
|
}
|
|
256
|
-
await
|
|
269
|
+
await waitForObservedRailProcess({ handle, projectDir, attempt_id });
|
|
257
270
|
}
|
|
258
271
|
} finally {
|
|
259
272
|
heartbeat.stop();
|
|
@@ -266,7 +279,7 @@ export async function executeReadyRailTask({
|
|
|
266
279
|
);
|
|
267
280
|
handle.retryCount = retries;
|
|
268
281
|
const execution = terminalEvidence(handle);
|
|
269
|
-
const base_commit_ref = dispatch.claim.attempt
|
|
282
|
+
const base_commit_ref = attemptExecutionBase(dispatch.claim.attempt);
|
|
270
283
|
const invalidOutput = execution.terminal_authority_receipt.execution_result_status === 'invalid_output';
|
|
271
284
|
const envelope = execution.exit_status === 0 && !invalidOutput
|
|
272
285
|
? parseRailExecutionEnvelope((handle.stdout || []).join(''))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Read a native helper against the original publisher attestation. Never
|
|
2
|
+
* execute or derive trust from the helper's current pathname/hash alone. */
|
|
3
|
+
import { createPublicKey } from 'node:crypto';
|
|
4
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { PACKAGE_ARTIFACT_MANIFEST, PACKAGE_ARTIFACT_SIGNATURE, verifyPackageArtifactManifest } from '../installer/package-artifact.js';
|
|
8
|
+
import { digestProviderExecutable } from '../installer-v2/provider-executable.js';
|
|
9
|
+
|
|
10
|
+
const SOURCE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
const INSTALLED = basename(SOURCE_ROOT) === '_cli';
|
|
12
|
+
const ARTIFACT_ROOT = INSTALLED ? SOURCE_ROOT : dirname(SOURCE_ROOT);
|
|
13
|
+
function fail(message) { throw Object.assign(new Error(message), { code: 'RUNTIME_NATIVE_HELPER_UNTRUSTED' }); }
|
|
14
|
+
function same(left, right) {
|
|
15
|
+
return ['dev', 'ino', 'mode', 'uid', 'gid', 'nlink', 'size', 'mtimeMs', 'ctimeMs']
|
|
16
|
+
.every((key) => left[key] === right[key]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readMetadata(path, maximum) {
|
|
20
|
+
let descriptor;
|
|
21
|
+
try {
|
|
22
|
+
if (realpathSync(path) !== path) fail('runtime attestation must use physical paths');
|
|
23
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
24
|
+
const before = fstatSync(descriptor);
|
|
25
|
+
if (!before.isFile() || before.nlink !== 1 || before.size < 1 || before.size > maximum) {
|
|
26
|
+
fail('runtime attestation must be a bounded single-link regular file');
|
|
27
|
+
}
|
|
28
|
+
const buffer = Buffer.alloc(maximum + 1);
|
|
29
|
+
let offset = 0;
|
|
30
|
+
while (offset < buffer.length) {
|
|
31
|
+
const count = readSync(descriptor, buffer, offset, buffer.length - offset, offset);
|
|
32
|
+
if (count === 0) break;
|
|
33
|
+
offset += count;
|
|
34
|
+
}
|
|
35
|
+
if (offset > maximum || offset !== before.size || !same(before, fstatSync(descriptor))
|
|
36
|
+
|| !same(before, lstatSync(path))) fail('runtime attestation changed while being read');
|
|
37
|
+
return buffer.subarray(0, offset).toString('utf8');
|
|
38
|
+
} finally { if (descriptor !== undefined) closeSync(descriptor); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function openAttestedRuntimeNativeHelper(name) {
|
|
42
|
+
let descriptor;
|
|
43
|
+
try {
|
|
44
|
+
if (!['src', '_cli'].includes(basename(SOURCE_ROOT))) fail('unknown runtime source layout');
|
|
45
|
+
if (!['linux', 'darwin'].includes(process.platform) || !['x64', 'arm64'].includes(process.arch)
|
|
46
|
+
|| !['execution-gate', 'darwin-launcher'].includes(name)
|
|
47
|
+
|| (name === 'darwin-launcher' && process.platform !== 'darwin')) fail('unsupported native helper target');
|
|
48
|
+
const manifest = JSON.parse(readMetadata(join(ARTIFACT_ROOT, PACKAGE_ARTIFACT_MANIFEST), 4 * 1024 * 1024));
|
|
49
|
+
const signature = readMetadata(join(ARTIFACT_ROOT, PACKAGE_ARTIFACT_SIGNATURE), 4096);
|
|
50
|
+
// No caller-selected key or environment trust override. The reader belongs
|
|
51
|
+
// to this runtime, whose own embedded production root is preserved at copy.
|
|
52
|
+
const key = createPublicKey(readMetadata(join(SOURCE_ROOT, 'installer/signing-public-key.pem'), 16384));
|
|
53
|
+
const verified = verifyPackageArtifactManifest(manifest, signature, key);
|
|
54
|
+
if (!verified.valid) fail(`runtime package attestation rejected: ${verified.reason}`);
|
|
55
|
+
const metadata = JSON.parse(readMetadata(join(ARTIFACT_ROOT, INSTALLED ? 'version.json' : 'package.json'), 65536));
|
|
56
|
+
const expectedVersion = INSTALLED ? metadata.package_version : metadata.version;
|
|
57
|
+
if (manifest.package_name !== 'chati-dev' || typeof expectedVersion !== 'string'
|
|
58
|
+
|| !expectedVersion || manifest.version !== expectedVersion
|
|
59
|
+
|| (!INSTALLED && metadata.name !== 'chati-dev')) fail('runtime package attestation version or name mismatch');
|
|
60
|
+
const entryPath = `node_modules/@chati/core/native/prebuilds/${process.platform}-${process.arch}/${name}`;
|
|
61
|
+
const entry = manifest.files[entryPath];
|
|
62
|
+
if (!entry || !/^[a-f0-9]{64}$/.test(entry.hash) || !Number.isSafeInteger(entry.size)
|
|
63
|
+
|| entry.size < 1 || entry.size > 64 * 1024 * 1024 || !Number.isInteger(entry.mode)
|
|
64
|
+
|| entry.mode < 0 || entry.mode > 0o777 || (entry.mode & 0o111) === 0
|
|
65
|
+
|| (entry.mode & 0o022) !== 0) fail('signed native helper entry is missing or invalid');
|
|
66
|
+
const path = join(ARTIFACT_ROOT, entryPath);
|
|
67
|
+
if (realpathSync(path) !== path) fail('native helper must use a physical path');
|
|
68
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
69
|
+
const before = fstatSync(descriptor);
|
|
70
|
+
if (!before.isFile() || before.nlink !== 1 || before.size !== entry.size
|
|
71
|
+
|| (before.mode & 0o777) !== entry.mode || ![0, process.geteuid()].includes(before.uid)) {
|
|
72
|
+
fail('native helper file does not match signed shape or safe ownership');
|
|
73
|
+
}
|
|
74
|
+
const executable = digestProviderExecutable(descriptor);
|
|
75
|
+
if (executable.sha256 !== entry.hash || !same(before, fstatSync(descriptor))
|
|
76
|
+
|| !same(before, lstatSync(path))) fail('native helper bytes differ from the publisher attestation');
|
|
77
|
+
const result = Object.freeze({ descriptor,
|
|
78
|
+
binding: Object.freeze({ executable_path: path, executable_sha256: entry.hash }), executable });
|
|
79
|
+
descriptor = undefined;
|
|
80
|
+
return result;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code === 'RUNTIME_NATIVE_HELPER_UNTRUSTED') throw error;
|
|
83
|
+
return fail(`native helper attestation is unavailable or invalid: ${error.code || error.name}`);
|
|
84
|
+
} finally { if (descriptor !== undefined) closeSync(descriptor); }
|
|
85
|
+
}
|