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
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It creates one provider-neutral installation artifact, inspects a target for
|
|
5
5
|
* legacy v1 markers, and returns a migration/rollback plan. Artifact writes
|
|
6
|
-
* are explicit through installV2 or authorized migrateV2.
|
|
7
|
-
*
|
|
6
|
+
* are explicit through installV2 or authorized migrateV2. Internal writes
|
|
7
|
+
* require the existing machine-license entitlement check. It never invokes a
|
|
8
|
+
* provider CLI or reads provider credentials.
|
|
8
9
|
*/
|
|
9
10
|
import { existsSync } from 'node:fs';
|
|
10
11
|
import { join, relative, resolve, sep } from 'node:path';
|
|
11
12
|
import { canonicalize, canonicalSerialize, sha256, ContractError } from '@chati/core';
|
|
12
13
|
import { validateInstallation } from '@chati/provider-registry';
|
|
14
|
+
import { selectCouncilReviewers } from '@chati/review-council';
|
|
13
15
|
import {
|
|
14
16
|
persistImmutableInstallationFile, readSafeInstallationStorageFile,
|
|
15
17
|
replaceInstallationStorageFile, verifyInstallationCatalogProvenance,
|
|
16
18
|
} from './catalog-client.js';
|
|
17
19
|
import { authorizeInstallationArtifact } from './installation-authority.js';
|
|
18
20
|
import { validateProviderExecutableBinding } from './provider-executable.js';
|
|
21
|
+
import { requireLicenseEntitlement } from '../license/client.js';
|
|
22
|
+
import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
|
|
19
23
|
|
|
20
24
|
export const INSTALLATION_SCHEMA_VERSION = 2;
|
|
21
25
|
export const INSTALLATION_ARTIFACT_PATH = '.chati/v2/installation.json';
|
|
@@ -56,10 +60,20 @@ const ARTIFACT_KEYS = new Set([
|
|
|
56
60
|
]);
|
|
57
61
|
const INSTALLATION_KEYS = new Set([
|
|
58
62
|
'installation_id', 'profile', 'enabled_providers', 'capability_snapshot_ref',
|
|
63
|
+
'quality_council',
|
|
59
64
|
// Read-only compatibility for early v2 artifacts. Current installers never
|
|
60
65
|
// emit these fields and runtime routing does not consume them.
|
|
61
66
|
'policy_ref', 'primary_binding', 'primary_harness', 'external_integrations',
|
|
62
67
|
]);
|
|
68
|
+
const QUALITY_COUNCIL_KEYS = new Set([
|
|
69
|
+
'mode', 'stages', 'min_members', 'max_members',
|
|
70
|
+
'require_distinct_providers', 'blocking_severities',
|
|
71
|
+
]);
|
|
72
|
+
const QUALITY_COUNCIL_STAGES = new Set(['discovery', 'planning', 'execution-design']);
|
|
73
|
+
// Artifacts created before this contract was introduced can still be loaded
|
|
74
|
+
// read-only. A current installer must never mint one: a caller-controlled
|
|
75
|
+
// clock is useful for deterministic artifacts, but is not evidence that an
|
|
76
|
+
// artifact really predates this security contract.
|
|
63
77
|
const BINDING_KEYS = new Set([
|
|
64
78
|
'provider_id', 'harness_id', 'allowed_models', 'allowed_reasoning_configurations',
|
|
65
79
|
'executable_path', 'executable_sha256', 'supporting_executables',
|
|
@@ -73,6 +87,9 @@ const MODEL_KEYS = new Set([
|
|
|
73
87
|
function assertClosedArtifactSchema(artifact) {
|
|
74
88
|
assertExactKeys(artifact, ARTIFACT_KEYS, 'artifact');
|
|
75
89
|
assertExactKeys(artifact.installation, INSTALLATION_KEYS, 'installation');
|
|
90
|
+
if (artifact.installation.quality_council !== undefined) {
|
|
91
|
+
assertExactKeys(artifact.installation.quality_council, QUALITY_COUNCIL_KEYS, 'quality council policy');
|
|
92
|
+
}
|
|
76
93
|
for (const binding of artifact.installation.enabled_providers ?? []) {
|
|
77
94
|
assertExactKeys(binding, BINDING_KEYS, 'enabled provider binding');
|
|
78
95
|
}
|
|
@@ -92,6 +109,44 @@ function assertClosedArtifactSchema(artifact) {
|
|
|
92
109
|
assertExactKeys(artifact.brain_read_capability, brainKeys, 'brain_read_capability');
|
|
93
110
|
}
|
|
94
111
|
|
|
112
|
+
function validateQualityCouncilConfiguration(configuration, { allowVerifiedLegacyArtifact = false } = {}) {
|
|
113
|
+
if (configuration === undefined) {
|
|
114
|
+
// Published releases up to 4.5.28 wrote v2 artifacts without a council
|
|
115
|
+
// policy whatever their date. Read paths (doctor, upgrade, runtime load)
|
|
116
|
+
// keep those verified artifacts inspectable and runnable; the runtime
|
|
117
|
+
// then reports COUNCIL_POLICY_NOT_CONFIGURED. Every creation and
|
|
118
|
+
// authorization path stays unconditional, so a new artifact can never
|
|
119
|
+
// omit the policy.
|
|
120
|
+
if (allowVerifiedLegacyArtifact) return;
|
|
121
|
+
throw new ContractError(
|
|
122
|
+
'QUALITY_COUNCIL_REQUIRED',
|
|
123
|
+
'current v2 installations require a complete quality_council policy; only verified pre-existing artifacts may omit it',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
assertExactKeys(configuration, QUALITY_COUNCIL_KEYS, 'quality council policy');
|
|
127
|
+
if (!['autonomous', 'guided'].includes(configuration.mode)) {
|
|
128
|
+
throw new ContractError('INVALID_COUNCIL_POLICY', 'quality_council.mode must be autonomous or guided');
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(configuration.stages) || configuration.stages.length !== QUALITY_COUNCIL_STAGES.size
|
|
131
|
+
|| !configuration.stages.every((stage) => QUALITY_COUNCIL_STAGES.has(stage))
|
|
132
|
+
|| new Set(configuration.stages).size !== QUALITY_COUNCIL_STAGES.size) {
|
|
133
|
+
throw new ContractError('INVALID_COUNCIL_POLICY', 'quality_council.stages must contain exactly discovery, planning and execution-design');
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isInteger(configuration.min_members) || configuration.min_members < 2
|
|
136
|
+
|| !Number.isInteger(configuration.max_members) || configuration.max_members < configuration.min_members
|
|
137
|
+
|| configuration.max_members > 3) {
|
|
138
|
+
throw new ContractError('INVALID_COUNCIL_POLICY', 'quality_council member bounds must be between 2 and 3');
|
|
139
|
+
}
|
|
140
|
+
if (configuration.require_distinct_providers !== true) {
|
|
141
|
+
throw new ContractError('INVALID_COUNCIL_POLICY', 'quality_council requires distinct providers');
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(configuration.blocking_severities) || configuration.blocking_severities.length !== 2
|
|
144
|
+
|| !configuration.blocking_severities.every((severity) => ['critical', 'high'].includes(severity))
|
|
145
|
+
|| new Set(configuration.blocking_severities).size !== 2) {
|
|
146
|
+
throw new ContractError('INVALID_COUNCIL_POLICY', 'quality_council blocking severities must contain exactly critical and high');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
95
150
|
function readClock(clock) {
|
|
96
151
|
if (typeof clock !== 'function') throw new ContractError('MISSING_CLOCK', 'a deterministic clock function is required');
|
|
97
152
|
const result = clock();
|
|
@@ -147,6 +202,10 @@ function artifactCore(input, createdAt) {
|
|
|
147
202
|
assertObject(input.capability_snapshot, 'INVALID_CATALOG', 'capability_snapshot');
|
|
148
203
|
assertString(input.project_id, 'INVALID_INSTALLATION', 'project_id');
|
|
149
204
|
validateInstallationExecutableBindings(input.installation);
|
|
205
|
+
// This is the creation boundary. Never derive a legacy exception from the
|
|
206
|
+
// supplied clock here, because install/reconfigure subsequently authorizes
|
|
207
|
+
// this exact artifact on the local host.
|
|
208
|
+
validateQualityCouncilConfiguration(input.installation.quality_council);
|
|
150
209
|
const installation = validateInstallation(input.installation, input.capability_snapshot, { clock: () => createdAt });
|
|
151
210
|
const brain_read_capability = normalizeBrainReadCapability(input.brain_read_capability);
|
|
152
211
|
const managed_paths = (input.managed_paths ?? [INSTALLATION_ARTIFACT_PATH]).map(ensureRelativePath);
|
|
@@ -212,6 +271,10 @@ export function doctorV2(artifact, { clock = () => new Date() } = {}) {
|
|
|
212
271
|
assertString(installation_artifact_id, 'INVALID_ARTIFACT', 'installation_artifact_id');
|
|
213
272
|
if (sha256(core) !== digest) throw new ContractError('INSTALLATION_ARTIFACT_DRIFT', 'installation artifact digest does not match its content');
|
|
214
273
|
validateInstallation(artifact.installation, artifact.capability_snapshot, { clock: () => readClock(clock) });
|
|
274
|
+
// The runtime additionally requires a valid pre-existing host receipt.
|
|
275
|
+
// doctor itself is read-only and therefore keeps old on-disk artifacts
|
|
276
|
+
// inspectable, while every current creation path above is unconditional.
|
|
277
|
+
validateQualityCouncilConfiguration(artifact.installation.quality_council, { allowVerifiedLegacyArtifact: true });
|
|
215
278
|
validateInstallationExecutableBindings(artifact.installation);
|
|
216
279
|
if (artifact.capability_snapshot.snapshot_id.startsWith('wizard-catalog-')
|
|
217
280
|
&& !artifact.capability_snapshot.source_catalog_ref) {
|
|
@@ -296,15 +359,61 @@ export function dryRunV2(input, { projectDir, clock = () => new Date(), exists =
|
|
|
296
359
|
.map(({ provider_id, harness_id }) => ({ code: 'V1_BINDING_NOT_SELECTED', provider_id, harness_id, message: 'legacy marker is not a selected v2 binding' }));
|
|
297
360
|
const pathOutsideProject = artifact.managed_paths.some((managedPath) => relative(resolve(projectDir), resolve(projectDir, managedPath)).split(sep).includes('..'));
|
|
298
361
|
if (pathOutsideProject) throw new ContractError('INVALID_MANAGED_PATH', 'managed path resolves outside project');
|
|
299
|
-
|
|
362
|
+
let council_readiness;
|
|
363
|
+
try {
|
|
364
|
+
const selection = assertExecutableV2Installation(artifact, { clock });
|
|
365
|
+
council_readiness = { ready: true, council_member_count: selection.member_count };
|
|
366
|
+
} catch (error) {
|
|
367
|
+
council_readiness = { ready: false, code: error.code || 'COUNCIL_READINESS_FAILED', message: error.message };
|
|
368
|
+
}
|
|
369
|
+
return Object.freeze(canonicalize({ schema_version: 1, mode: 'dry-run', writes_performed: false, artifact, doctor, detector, backup_plan, rollback_plan, incompatibilities, council_readiness }));
|
|
300
370
|
}
|
|
301
371
|
|
|
302
|
-
/**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Read-only operational precondition for new writes, shared with the wizard
|
|
374
|
+
* before it installs framework files. Historical guided artifacts stay readable
|
|
375
|
+
* but require explicit reconfiguration, never an implicit provider addition.
|
|
376
|
+
* This proves council eligibility, not CLI authentication or end-to-end health.
|
|
377
|
+
*/
|
|
378
|
+
export function assertExecutableV2Installation(artifact, { clock = () => new Date() } = {}) {
|
|
379
|
+
if (artifact?.installation?.quality_council?.mode !== 'autonomous') {
|
|
380
|
+
throw new ContractError('GUIDED_COUNCIL_REQUIRED',
|
|
381
|
+
'installation requires an autonomous council with at least two distinct selected providers capable of review; explicitly reconfigure guided installations');
|
|
382
|
+
}
|
|
383
|
+
return selectCouncilReviewers({
|
|
384
|
+
installation: artifact.installation,
|
|
385
|
+
snapshot: artifact.capability_snapshot,
|
|
386
|
+
policy: artifact.installation.quality_council,
|
|
387
|
+
clock,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function authorizeV2Write(report, { projectDir, clock, licenseValidator }) {
|
|
307
392
|
verifyInstallationCatalogProvenance(projectDir, report.artifact.capability_snapshot, { clock });
|
|
393
|
+
assertExecutableV2Installation(report.artifact, { clock });
|
|
394
|
+
if (report.artifact.installation.profile === 'authorized-internal') {
|
|
395
|
+
// A local profile/cache is not entitlement. Await the same authoritative
|
|
396
|
+
// check as the wizard before backups, host signatures or artifact writes.
|
|
397
|
+
await requireLicenseEntitlement('internal', { validate: licenseValidator });
|
|
398
|
+
verifyInstallationCatalogProvenance(projectDir, report.artifact.capability_snapshot, { clock });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function withAuthorizedV2Write(report, options, action) {
|
|
403
|
+
// Read-only preflight precedes even maintenance bookkeeping: denied Internal
|
|
404
|
+
// authorization must leave both the project and host authority unchanged.
|
|
405
|
+
await authorizeV2Write(report, options);
|
|
406
|
+
return withUpgradeMaintenance(options.projectDir, async () => {
|
|
407
|
+
// Provenance may have changed during the authorization await. No backup,
|
|
408
|
+
// signature or artifact write is permitted before this check and the lease.
|
|
409
|
+
verifyInstallationCatalogProvenance(options.projectDir, report.artifact.capability_snapshot, { clock: options.clock });
|
|
410
|
+
assertExecutableV2Installation(report.artifact, { clock: options.clock });
|
|
411
|
+
return action();
|
|
412
|
+
}, { createState: true });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Private persistence primitive, reached only after authorizeV2Write. */
|
|
416
|
+
function persistAuthorizedV2Report(report, { projectDir, authorityDirectory }) {
|
|
308
417
|
const artifactPath = join(projectDir, INSTALLATION_ARTIFACT_PATH);
|
|
309
418
|
const serialized = `${canonicalSerialize(report.artifact)}\n`;
|
|
310
419
|
const existing = readSafeInstallationStorageFile(artifactPath, { allowMissing: true });
|
|
@@ -330,19 +439,32 @@ export function installV2(input, {
|
|
|
330
439
|
}));
|
|
331
440
|
}
|
|
332
441
|
|
|
442
|
+
/** Explicit v2 installation, including Internal entitlement before persistence. */
|
|
443
|
+
export async function installV2(input, {
|
|
444
|
+
projectDir, clock = () => new Date(), exists = existsSync, authorityDirectory, licenseValidator,
|
|
445
|
+
} = {}) {
|
|
446
|
+
const report = dryRunV2(input, { projectDir, clock, exists });
|
|
447
|
+
return withAuthorizedV2Write(report, { projectDir, clock, licenseValidator },
|
|
448
|
+
() => persistAuthorizedV2Report(report, { projectDir, authorityDirectory }));
|
|
449
|
+
}
|
|
450
|
+
|
|
333
451
|
/**
|
|
334
452
|
* Explicitly replaces an existing v2 artifact after preserving its exact
|
|
335
453
|
* bytes in the project-local backup store. Provider-specific files are never
|
|
336
454
|
* read or rewritten by this operation.
|
|
337
455
|
*/
|
|
338
|
-
export function reconfigureV2(input, {
|
|
339
|
-
projectDir, clock = () => new Date(), exists = existsSync, authorityDirectory,
|
|
456
|
+
export async function reconfigureV2(input, {
|
|
457
|
+
projectDir, clock = () => new Date(), exists = existsSync, authorityDirectory, licenseValidator,
|
|
340
458
|
} = {}) {
|
|
341
459
|
const report = dryRunV2(input, { projectDir, clock, exists });
|
|
342
|
-
|
|
460
|
+
return withAuthorizedV2Write(report, { projectDir, clock, licenseValidator }, () =>
|
|
461
|
+
reconfigureAuthorizedV2Report(report, { projectDir, authorityDirectory }));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function reconfigureAuthorizedV2Report(report, { projectDir, authorityDirectory }) {
|
|
343
465
|
const artifactPath = join(projectDir, INSTALLATION_ARTIFACT_PATH);
|
|
344
466
|
const existing = readSafeInstallationStorageFile(artifactPath, { allowMissing: true });
|
|
345
|
-
if (existing === null) return
|
|
467
|
+
if (existing === null) return persistAuthorizedV2Report(report, { projectDir, authorityDirectory });
|
|
346
468
|
let existingArtifact;
|
|
347
469
|
try { existingArtifact = JSON.parse(existing.content); } catch (error) {
|
|
348
470
|
throw new ContractError('EXISTING_INSTALLATION_CORRUPT', `existing v2 artifact is not valid JSON: ${error.message}`);
|
|
@@ -374,14 +496,18 @@ export function reconfigureV2(input, {
|
|
|
374
496
|
* legacy markers, writes the provider-neutral artifact, and never rewrites a
|
|
375
497
|
* provider-specific configuration file.
|
|
376
498
|
*/
|
|
377
|
-
export function migrateV2(input, {
|
|
378
|
-
projectDir, authorize = false, clock = () => new Date(), exists = existsSync, authorityDirectory,
|
|
499
|
+
export async function migrateV2(input, {
|
|
500
|
+
projectDir, authorize = false, clock = () => new Date(), exists = existsSync, authorityDirectory, licenseValidator,
|
|
379
501
|
} = {}) {
|
|
380
502
|
const preview = dryRunV2(input, { projectDir, clock, exists });
|
|
381
503
|
if (authorize !== true) {
|
|
382
504
|
return Object.freeze(canonicalize({ ...preview, mode: 'migration-preview', writes_performed: false, requires_explicit_authorization: true }));
|
|
383
505
|
}
|
|
506
|
+
return withAuthorizedV2Write(preview, { projectDir, clock, licenseValidator }, () =>
|
|
507
|
+
migrateAuthorizedV2Report(preview, { projectDir, authorityDirectory }));
|
|
508
|
+
}
|
|
384
509
|
|
|
510
|
+
function migrateAuthorizedV2Report(preview, { projectDir, authorityDirectory }) {
|
|
385
511
|
const artifactPath = join(projectDir, INSTALLATION_ARTIFACT_PATH);
|
|
386
512
|
const existingArtifact = readSafeInstallationStorageFile(artifactPath, { allowMissing: true });
|
|
387
513
|
if (existingArtifact !== null) {
|
|
@@ -403,7 +529,7 @@ export function migrateV2(input, {
|
|
|
403
529
|
conflictCode: 'MIGRATION_BACKUP_CONFLICT', label: 'legacy migration backup',
|
|
404
530
|
})) backed_up.push(entry);
|
|
405
531
|
}
|
|
406
|
-
const installed =
|
|
532
|
+
const installed = persistAuthorizedV2Report(preview, { projectDir, authorityDirectory });
|
|
407
533
|
return Object.freeze(canonicalize({
|
|
408
534
|
...preview,
|
|
409
535
|
mode: 'migrated',
|
|
@@ -3,13 +3,14 @@ import {
|
|
|
3
3
|
sign as cryptoSign, verify as cryptoVerify,
|
|
4
4
|
} from 'node:crypto';
|
|
5
5
|
import { userInfo } from 'node:os';
|
|
6
|
-
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
7
7
|
import { lstatSync, realpathSync } from 'node:fs';
|
|
8
8
|
import { canonicalSerialize, ContractError } from '@chati/core';
|
|
9
9
|
import {
|
|
10
10
|
persistImmutableInstallationFile, readSafeInstallationStorageFile,
|
|
11
11
|
} from './catalog-client.js';
|
|
12
12
|
|
|
13
|
+
export { projectBinding as installationAuthorityProjectBinding };
|
|
13
14
|
export const INSTALLATION_AUTHORITY_RECEIPT_VERSION = 1;
|
|
14
15
|
export const INSTALLATION_AUTHORITY_RECEIPT_TYPE = 'chati-installation-authority';
|
|
15
16
|
export const INSTALLATION_AUTHORITY_DIRECTORY = '.chati/v2/authorities';
|
|
@@ -66,7 +67,7 @@ function assertPrivateKeyPermissions(source) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
function readOrCreateAuthorityKeyPair(authorityDirectory) {
|
|
70
|
+
function readOrCreateAuthorityKeyPair(authorityDirectory, { create = true } = {}) {
|
|
70
71
|
const root = authorityRoot(authorityDirectory);
|
|
71
72
|
const privatePath = join(root, PRIVATE_KEY_FILENAME);
|
|
72
73
|
const publicPath = installationAuthorityPublicKeyPath(authorityDirectory);
|
|
@@ -74,6 +75,7 @@ function readOrCreateAuthorityKeyPair(authorityDirectory) {
|
|
|
74
75
|
allowMissing: true, label: 'installation authority private key', encoding: 'utf8', maxBytes: 64 * 1024,
|
|
75
76
|
});
|
|
76
77
|
if (privateSource === null) {
|
|
78
|
+
if (!create) fail('DELEGATED_AUTHORITY_IDENTITY_REQUIRED', 'create a public certification request before using the delegated authority');
|
|
77
79
|
const generated = generateKeyPairSync('ed25519', {
|
|
78
80
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
79
81
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
@@ -204,7 +206,34 @@ function unsignedReceipt(receipt) {
|
|
|
204
206
|
export function authorizeInstallationArtifact(projectDir, artifact, {
|
|
205
207
|
authorityDirectory,
|
|
206
208
|
} = {}) {
|
|
209
|
+
// A host receipt is minted only by a current explicit install or
|
|
210
|
+
// reconfigure action. It must not be usable to manufacture a "historical"
|
|
211
|
+
// policy-less installation by passing an old deterministic clock.
|
|
212
|
+
if (artifact?.installation?.quality_council === undefined) {
|
|
213
|
+
fail('QUALITY_COUNCIL_REQUIRED', 'current host authority refuses to authorize an installation without quality_council');
|
|
214
|
+
}
|
|
207
215
|
const authority = readOrCreateAuthorityKeyPair(authorityDirectory);
|
|
216
|
+
const path = receiptPath(projectDir, artifact);
|
|
217
|
+
const existing = readSafeInstallationStorageFile(path, {
|
|
218
|
+
allowMissing: true, label: 'installation authority receipt', encoding: 'utf8', maxBytes: 256 * 1024,
|
|
219
|
+
});
|
|
220
|
+
if (existing !== null) {
|
|
221
|
+
let receipt;
|
|
222
|
+
try { receipt = JSON.parse(existing.content); }
|
|
223
|
+
catch { fail('INVALID_INSTALLATION_AUTHORITY_RECEIPT', 'installation authority receipt is not valid JSON'); }
|
|
224
|
+
try {
|
|
225
|
+
verifyInstallationArtifactAuthority(projectDir, artifact, { authorityDirectory });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error?.code === 'INSTALLATION_AUTHORITY_MISMATCH') {
|
|
228
|
+
fail('INSTALLATION_AUTHORITY_RECEIPT_CONFLICT', 'installation authority receipt exists for a different host authority');
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
receipt: Object.freeze(receipt),
|
|
234
|
+
receipt_path: relative(resolve(projectDir), path).replaceAll('\\', '/'),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
208
237
|
const payload = {
|
|
209
238
|
schema_version: INSTALLATION_AUTHORITY_RECEIPT_VERSION,
|
|
210
239
|
receipt_type: INSTALLATION_AUTHORITY_RECEIPT_TYPE,
|
|
@@ -213,13 +242,14 @@ export function authorizeInstallationArtifact(projectDir, artifact, {
|
|
|
213
242
|
installation_artifact_id: artifact.installation_artifact_id,
|
|
214
243
|
installation_digest: artifact.digest,
|
|
215
244
|
artifact_created_at: artifact.created_at,
|
|
216
|
-
|
|
245
|
+
// This is an actual authority issue time, never the artifact's
|
|
246
|
+
// caller-supplied deterministic timestamp.
|
|
247
|
+
issued_at: new Date().toISOString(),
|
|
217
248
|
};
|
|
218
249
|
const receipt = Object.freeze({
|
|
219
250
|
...payload,
|
|
220
251
|
signature: cryptoSign(null, Buffer.from(canonicalSerialize(payload)), authority.privateKey).toString('base64'),
|
|
221
252
|
});
|
|
222
|
-
const path = receiptPath(projectDir, artifact);
|
|
223
253
|
persistImmutableInstallationFile(path, `${canonicalSerialize(receipt)}\n`, {
|
|
224
254
|
conflictCode: 'INSTALLATION_AUTHORITY_RECEIPT_CONFLICT',
|
|
225
255
|
label: 'installation authority receipt', maxBytes: 256 * 1024,
|
|
@@ -282,6 +312,70 @@ export function installationAuthorityStateDirectory(authorityDirectory) {
|
|
|
282
312
|
return authorityRoot(authorityDirectory);
|
|
283
313
|
}
|
|
284
314
|
|
|
315
|
+
function delegatedAuthority({ projectDir, projectId, kind, authorityDirectory, create = false } = {}) {
|
|
316
|
+
if (!['rail-evidence', 'clickup-mcp'].includes(kind) || typeof projectId !== 'string' || !projectId.trim()) {
|
|
317
|
+
fail('INVALID_DELEGATED_AUTHORITY', 'delegated authority requires a project and a supported evidence kind');
|
|
318
|
+
}
|
|
319
|
+
const root = authorityRoot(authorityDirectory);
|
|
320
|
+
const project = realpathSync(resolve(projectDir));
|
|
321
|
+
if (root === project || root.startsWith(`${project}${sep}`) || project.startsWith(`${root}${sep}`)) {
|
|
322
|
+
fail('DELEGATED_AUTHORITY_PROJECT_OVERLAP', 'delegated private authority storage must not overlap the project');
|
|
323
|
+
}
|
|
324
|
+
const binding = projectBinding(project, projectId);
|
|
325
|
+
const bindingDigest = createHash('sha256').update(canonicalSerialize({ kind, binding })).digest('hex');
|
|
326
|
+
const delegatedRoot = join(root, 'evidence-issuers', bindingDigest);
|
|
327
|
+
const authority = readOrCreateAuthorityKeyPair(delegatedRoot, { create });
|
|
328
|
+
return { ...authority, bindingDigest,
|
|
329
|
+
issuer_id: `local-${kind}://${keyFingerprint(authority.publicKey)}/${bindingDigest}` };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** A distinct delegated key, not an additional root of trust. Public output only. */
|
|
333
|
+
export function delegatedAuthorityPublicIdentity(options) {
|
|
334
|
+
const authority = delegatedAuthority(options);
|
|
335
|
+
return Object.freeze({
|
|
336
|
+
issuer_id: authority.issuer_id, project_binding_digest: authority.bindingDigest,
|
|
337
|
+
public_key: authority.publicKey.export({ type: 'spki', format: 'pem' }),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Internal issuer primitive. No CLI accepts arbitrary statements to sign. */
|
|
342
|
+
export function signDelegatedAuthorityStatement(statement, options) {
|
|
343
|
+
const authority = delegatedAuthority({ ...options, create: false });
|
|
344
|
+
return Object.freeze({ issuer_id: authority.issuer_id,
|
|
345
|
+
signature: cryptoSign(null, Buffer.from(canonicalSerialize(statement)), authority.privateKey).toString('base64') });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Internal ClickUp observer primitive, not a CLI accepting results to sign.
|
|
349
|
+
* The existing v2 MCP receipt verifier uses this ordered JSON wire format,
|
|
350
|
+
* whereas certification requests and RAIL statements use canonical JSON.
|
|
351
|
+
* Keep those domains distinct; never export or recreate the delegated key.
|
|
352
|
+
*/
|
|
353
|
+
export function signDelegatedClickUpMcpPayload(payload, options) {
|
|
354
|
+
const fields = ['schema_version', 'issuer_id', 'kind', 'projection_id', 'clickup_ref',
|
|
355
|
+
'idempotency_key', 'operation', 'delivery_attempt', 'delivery_attempted_at', 'observed_at', 'response_digest'];
|
|
356
|
+
const invalid = () => fail('INVALID_CLICKUP_SIGNING_PAYLOAD', 'ClickUp observation requires its exact v2 signing payload');
|
|
357
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
|
358
|
+
|| ![null, Object.prototype].includes(Object.getPrototypeOf(payload))
|
|
359
|
+
|| Object.getOwnPropertySymbols(payload).length
|
|
360
|
+
|| Object.keys(payload).sort().join(',') !== [...fields].sort().join(',')) invalid();
|
|
361
|
+
const descriptors = Object.getOwnPropertyDescriptors(payload);
|
|
362
|
+
if (fields.some((name) => !Object.hasOwn(descriptors[name], 'value'))) invalid();
|
|
363
|
+
// Reconstruct the verifier's exact order, independent of caller insertion order.
|
|
364
|
+
const statement = Object.fromEntries(fields.map((name) => [name, descriptors[name].value]));
|
|
365
|
+
if (statement.schema_version !== 2 || !['clickup_ack', 'clickup_lookup'].includes(statement.kind)
|
|
366
|
+
|| !['create', 'update', 'close'].includes(statement.operation)
|
|
367
|
+
|| !Number.isSafeInteger(statement.delivery_attempt) || statement.delivery_attempt < 1
|
|
368
|
+
|| !fields.filter((name) => !['schema_version', 'delivery_attempt'].includes(name))
|
|
369
|
+
.every((name) => typeof statement[name] === 'string' && statement[name].length > 0 && statement[name].length <= 8192)
|
|
370
|
+
|| !/^[a-f0-9]{64}$/.test(statement.response_digest)
|
|
371
|
+
|| !Number.isFinite(Date.parse(statement.delivery_attempted_at)) || !Number.isFinite(Date.parse(statement.observed_at))
|
|
372
|
+
|| (options?.kind !== undefined && options.kind !== 'clickup-mcp')) invalid();
|
|
373
|
+
const authority = delegatedAuthority({ ...options, kind: 'clickup-mcp', create: false });
|
|
374
|
+
if (statement.issuer_id !== authority.issuer_id) invalid();
|
|
375
|
+
return Object.freeze({ issuer_id: authority.issuer_id,
|
|
376
|
+
signature: cryptoSign(null, Buffer.from(JSON.stringify(statement)), authority.privateKey).toString('base64') });
|
|
377
|
+
}
|
|
378
|
+
|
|
285
379
|
/** Sign a domain-separated payload with the persistent host authority. */
|
|
286
380
|
export function signHostAuthorityPayload(payload, {
|
|
287
381
|
domain,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync,
|
|
4
4
|
} from 'node:fs';
|
|
5
5
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
6
7
|
|
|
7
8
|
const MAX_PROVIDER_EXECUTABLE_BYTES = 1024 * 1024 * 1024;
|
|
8
9
|
const PROVIDER_EXECUTABLE_DIGEST = /^[a-f0-9]{64}$/;
|
|
@@ -14,6 +15,11 @@ const CODEX_NATIVE_TARGETS = Object.freeze({
|
|
|
14
15
|
'darwin/arm64': Object.freeze(['@openai/codex-darwin-arm64', 'aarch64-apple-darwin']),
|
|
15
16
|
});
|
|
16
17
|
|
|
18
|
+
/** A sealed native Grok launch must retain its npm launcher's managed marker. */
|
|
19
|
+
export function sealedProviderRuntimeEnvironment(harnessId) {
|
|
20
|
+
return Object.freeze(harnessId === 'grok' ? { GROK_MANAGED_BY_NPM: '1' } : {});
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
function fail(code, message) {
|
|
18
24
|
throw Object.assign(new Error(message), { code });
|
|
19
25
|
}
|
|
@@ -87,19 +93,106 @@ function codexNativeExecutable(wrapperPath, { platform = process.platform, arch
|
|
|
87
93
|
return fail('PROVIDER_EXECUTABLE_NOT_FOUND', 'Codex wrapper does not resolve to its installed native executable');
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
function
|
|
91
|
-
if (harnessId !== 'codex') return resolvedCommand;
|
|
96
|
+
function executablePrefix(path) {
|
|
92
97
|
let descriptor;
|
|
93
|
-
let prefix;
|
|
94
98
|
try {
|
|
95
|
-
descriptor = openSync(
|
|
99
|
+
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
100
|
+
assertExecutableStat(fstatSync(descriptor));
|
|
96
101
|
const buffer = Buffer.alloc(64);
|
|
97
102
|
const bytes = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
98
|
-
|
|
103
|
+
return buffer.subarray(0, bytes);
|
|
104
|
+
} finally {
|
|
105
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isNativeImage(prefix) {
|
|
110
|
+
return ['7f454c46', 'feedface', 'cefaedfe', 'feedfacf', 'cffaedfe', 'cafebabe', 'bebafeca', 'cafebabf', 'bfbafeca']
|
|
111
|
+
.includes(prefix.subarray(0, 4).toString('hex'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function knownNpmLauncherRoot(harnessId, path) {
|
|
115
|
+
const expected = harnessId === 'claude' ? '@anthropic-ai/claude-code' : '@xai-official/grok';
|
|
116
|
+
const root = dirname(dirname(path));
|
|
117
|
+
if (basename(dirname(path)) !== 'bin'
|
|
118
|
+
|| `${basename(dirname(root))}/${basename(root)}` !== expected) return null;
|
|
119
|
+
let descriptor;
|
|
120
|
+
try {
|
|
121
|
+
descriptor = openSync(join(root, 'package.json'), constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
122
|
+
const before = fstatSync(descriptor);
|
|
123
|
+
if (!before.isFile() || before.size > 64 * 1024) fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'provider package metadata must be a bounded regular file');
|
|
124
|
+
const bytes = Buffer.alloc((64 * 1024) + 1);
|
|
125
|
+
let offset = 0;
|
|
126
|
+
while (offset < bytes.length) {
|
|
127
|
+
const count = readSync(descriptor, bytes, offset, bytes.length - offset, null);
|
|
128
|
+
if (!count) break;
|
|
129
|
+
offset += count;
|
|
130
|
+
}
|
|
131
|
+
if (offset > 64 * 1024 || !sameSnapshot(snapshot(before), snapshot(fstatSync(descriptor)))) {
|
|
132
|
+
fail('PROVIDER_EXECUTABLE_DRIFT', 'provider package metadata changed during resolution');
|
|
133
|
+
}
|
|
134
|
+
const metadata = JSON.parse(bytes.subarray(0, offset).toString('utf8'));
|
|
135
|
+
if (metadata.name !== expected || typeof metadata.bin?.[harnessId] !== 'string'
|
|
136
|
+
|| resolve(root, metadata.bin[harnessId]) !== path) {
|
|
137
|
+
fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'provider launcher does not match its installed package metadata');
|
|
138
|
+
}
|
|
139
|
+
return root;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error?.code?.startsWith('PROVIDER_EXECUTABLE_')) throw error;
|
|
142
|
+
return fail('PROVIDER_EXECUTABLE_BINDING_INVALID', `provider package metadata is unavailable or invalid: ${error.code || error.name}`);
|
|
99
143
|
} finally {
|
|
100
144
|
if (descriptor !== undefined) closeSync(descriptor);
|
|
101
145
|
}
|
|
102
|
-
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function installedNativeCandidate(path) {
|
|
149
|
+
let resolved;
|
|
150
|
+
try { resolved = realpathSync(path); }
|
|
151
|
+
catch (error) { if (error?.code === 'ENOENT') return null; throw error; }
|
|
152
|
+
if (!isNativeImage(executablePrefix(resolved))) {
|
|
153
|
+
fail('PROVIDER_EXECUTABLE_BINDING_INVALID', 'provider native image resolved to a script or unsupported executable format');
|
|
154
|
+
}
|
|
155
|
+
return resolved;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function packageNativeExecutable(harnessId, root, { platform, arch, grokHome, libc }) {
|
|
159
|
+
if (!['linux', 'darwin'].includes(platform) || !['x64', 'arm64'].includes(arch)) {
|
|
160
|
+
fail('PROVIDER_EXECUTABLE_UNSUPPORTED', `${harnessId} native binding is unsupported on ${platform}/${arch}`);
|
|
161
|
+
}
|
|
162
|
+
if (harnessId === 'grok') {
|
|
163
|
+
// Follow the npm launcher's existing canonical installation, without
|
|
164
|
+
// running its decompression/bootstrap or changing any global CLI state.
|
|
165
|
+
const canonical = installedNativeCandidate(join(grokHome, 'bin', 'grok'));
|
|
166
|
+
if (canonical) return canonical;
|
|
167
|
+
}
|
|
168
|
+
if (platform === 'linux' && !['glibc', 'musl'].includes(libc)) {
|
|
169
|
+
fail('PROVIDER_EXECUTABLE_UNSUPPORTED', 'native provider binding requires a supported Linux libc');
|
|
170
|
+
}
|
|
171
|
+
const musl = harnessId === 'claude' && platform === 'linux' && libc === 'musl';
|
|
172
|
+
const scope = harnessId === 'claude' ? '@anthropic-ai' : '@xai-official';
|
|
173
|
+
const packageName = `${harnessId === 'claude' ? 'claude-code' : 'grok'}-${platform}-${arch}${musl ? '-musl' : ''}`;
|
|
174
|
+
const entry = harnessId === 'claude' ? 'claude' : join('bin', 'grok');
|
|
175
|
+
for (const candidate of [join(root, 'node_modules', scope, packageName, entry), join(dirname(root), packageName, entry)]) {
|
|
176
|
+
const native = installedNativeCandidate(candidate);
|
|
177
|
+
if (native) return native;
|
|
178
|
+
}
|
|
179
|
+
return fail('PROVIDER_EXECUTABLE_NOT_FOUND', `${harnessId} has no installed native image; complete that CLI's own installation before retrying Chati`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function effectiveProviderExecutable(harnessId, resolvedCommand, options) {
|
|
183
|
+
const prefix = executablePrefix(resolvedCommand);
|
|
184
|
+
if (isNativeImage(prefix)) return resolvedCommand;
|
|
185
|
+
if (harnessId === 'claude' || harnessId === 'grok') {
|
|
186
|
+
const packageRoot = knownNpmLauncherRoot(harnessId, resolvedCommand);
|
|
187
|
+
if (packageRoot) return packageNativeExecutable(harnessId, packageRoot, options);
|
|
188
|
+
}
|
|
189
|
+
// Resolving a multicall shell alias loses its invocation name when sealed
|
|
190
|
+
// descriptors are executed. Never silently bind the shared dispatcher.
|
|
191
|
+
if (![harnessId, `${harnessId}.js`, `${harnessId}.cjs`, `${harnessId}.mjs`, `${harnessId}.exe`].includes(basename(resolvedCommand))) {
|
|
192
|
+
fail('PROVIDER_EXECUTABLE_WRAPPER_UNSUPPORTED', `${harnessId} resolves to a non-native shared or renamed launcher; expose the installed provider executable in the installation PATH`);
|
|
193
|
+
}
|
|
194
|
+
return harnessId === 'codex' && prefix.toString('utf8').startsWith('#!')
|
|
195
|
+
? codexNativeExecutable(resolvedCommand, options) : resolvedCommand;
|
|
103
196
|
}
|
|
104
197
|
|
|
105
198
|
function digestExecutablePath(executablePath) {
|
|
@@ -107,7 +200,7 @@ function digestExecutablePath(executablePath) {
|
|
|
107
200
|
try {
|
|
108
201
|
const pathStat = lstatSync(executablePath);
|
|
109
202
|
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));
|
|
203
|
+
descriptor = openSync(executablePath, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
111
204
|
const pinned = fstatSync(descriptor);
|
|
112
205
|
if (pathStat.dev !== pinned.dev || pathStat.ino !== pinned.ino) {
|
|
113
206
|
fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable changed before descriptor pinning');
|
|
@@ -133,6 +226,8 @@ export function resolveProviderExecutableBinding(harnessId, {
|
|
|
133
226
|
pathEnv = process.env.PATH,
|
|
134
227
|
platform = process.platform,
|
|
135
228
|
arch = process.arch,
|
|
229
|
+
grokHome = process.env.GROK_HOME ?? join(homedir(), '.grok'),
|
|
230
|
+
libc = process.platform === 'linux' && !process.report?.getReport()?.header?.glibcVersionRuntime ? 'musl' : 'glibc',
|
|
136
231
|
} = {}) {
|
|
137
232
|
if (!['claude', 'codex', 'grok'].includes(harnessId)) {
|
|
138
233
|
fail('PROVIDER_EXECUTABLE_UNSUPPORTED', `provider executable binding is unsupported for ${harnessId}`);
|
|
@@ -140,7 +235,7 @@ export function resolveProviderExecutableBinding(harnessId, {
|
|
|
140
235
|
const executable_path = effectiveProviderExecutable(
|
|
141
236
|
harnessId,
|
|
142
237
|
resolveFromPath(harnessId, pathEnv),
|
|
143
|
-
{ platform, arch },
|
|
238
|
+
{ platform, arch, grokHome, libc },
|
|
144
239
|
);
|
|
145
240
|
const digest = digestExecutablePath(executable_path);
|
|
146
241
|
return Object.freeze({
|
|
@@ -201,13 +296,13 @@ function assertOutsideProject(executablePath, projectDir) {
|
|
|
201
296
|
}
|
|
202
297
|
}
|
|
203
298
|
|
|
204
|
-
function openPinnedExecutable(reference, { projectDir } = {}) {
|
|
299
|
+
function openPinnedExecutable(reference, { projectDir, requireNativeImage = false } = {}) {
|
|
205
300
|
assertOutsideProject(reference.executable_path, projectDir);
|
|
206
301
|
let descriptor;
|
|
207
302
|
try {
|
|
208
303
|
const pathStat = lstatSync(reference.executable_path);
|
|
209
304
|
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));
|
|
305
|
+
descriptor = openSync(reference.executable_path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
211
306
|
const pinned = fstatSync(descriptor);
|
|
212
307
|
if (pathStat.dev !== pinned.dev || pathStat.ino !== pinned.ino) {
|
|
213
308
|
fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable changed before descriptor pinning');
|
|
@@ -216,6 +311,16 @@ function openPinnedExecutable(reference, { projectDir } = {}) {
|
|
|
216
311
|
if (executable.sha256 !== reference.executable_sha256) {
|
|
217
312
|
fail('PROVIDER_EXECUTABLE_DRIFT', 'provider executable digest differs from the sealed installation binding');
|
|
218
313
|
}
|
|
314
|
+
if (requireNativeImage) {
|
|
315
|
+
const prefix = Buffer.alloc(4);
|
|
316
|
+
const count = readSync(descriptor, prefix, 0, prefix.length, 0);
|
|
317
|
+
if (!sameSnapshot(snapshot(fstatSync(descriptor)), executable.snapshot)) {
|
|
318
|
+
fail('PROVIDER_EXECUTABLE_DRIFT', 'verification interpreter changed during native-format validation');
|
|
319
|
+
}
|
|
320
|
+
if (count !== prefix.length || !isNativeImage(prefix)) {
|
|
321
|
+
fail('PROVIDER_EXECUTABLE_NATIVE_REQUIRED', 'verification checks require a pinned native interpreter, not a shebang wrapper');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
219
324
|
return Object.freeze({ descriptor, binding: reference, executable });
|
|
220
325
|
} catch (error) {
|
|
221
326
|
if (descriptor !== undefined) closeSync(descriptor);
|
|
@@ -224,9 +329,9 @@ function openPinnedExecutable(reference, { projectDir } = {}) {
|
|
|
224
329
|
}
|
|
225
330
|
}
|
|
226
331
|
|
|
227
|
-
export function openPinnedProviderExecutable(binding, { harnessId, projectDir } = {}) {
|
|
332
|
+
export function openPinnedProviderExecutable(binding, { harnessId, projectDir, requireNativeImage = false } = {}) {
|
|
228
333
|
const validated = validateProviderExecutableBinding(binding, { harnessId });
|
|
229
|
-
return openPinnedExecutable(validated, { projectDir });
|
|
334
|
+
return openPinnedExecutable(validated, { projectDir, requireNativeImage });
|
|
230
335
|
}
|
|
231
336
|
|
|
232
337
|
export function openPinnedProviderSupportingExecutables(binding, { harnessId, projectDir } = {}) {
|
|
@@ -104,6 +104,17 @@ export function buildWizardV2InstallationInput({
|
|
|
104
104
|
enabled_providers,
|
|
105
105
|
capability_snapshot_ref: snapshotId,
|
|
106
106
|
policy_ref: `policy://installation/${snapshotId}`,
|
|
107
|
+
// Preserve a descriptive guided artifact for previews and historical reads.
|
|
108
|
+
// New executable writes require autonomous mode and an eligible quorum;
|
|
109
|
+
// the installer enforces this without inventing an unselected reviewer.
|
|
110
|
+
quality_council: {
|
|
111
|
+
mode: providers.length >= 2 ? 'autonomous' : 'guided',
|
|
112
|
+
stages: ['discovery', 'planning', 'execution-design'],
|
|
113
|
+
min_members: 2,
|
|
114
|
+
max_members: 3,
|
|
115
|
+
require_distinct_providers: true,
|
|
116
|
+
blocking_severities: ['critical', 'high'],
|
|
117
|
+
},
|
|
107
118
|
...(installationMode === 'internal' ? { external_integrations: { clickup: 'required' } } : {}),
|
|
108
119
|
};
|
|
109
120
|
return Object.freeze({
|