@starci/hfs 4.8.0 → 4.9.1
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/CHANGELOG.md +10 -0
- package/emit/operation-schemas.mjs +3 -0
- package/lint/bound-sys.cjs +2 -2
- package/package.json +1 -1
- package/runtime/engine/admission.mjs +1 -1
- package/runtime/engine/config.mjs +2 -2
- package/runtime/engine/db/machine.mjs +2 -3
- package/runtime/engine/invalid-config.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
- package/runtime/knowledge/hfs/rules.yaml +23 -0
- package/runtime/modules/kernel/failure-codes.yaml +10 -0
- package/runtime/scripts/guards/git-policy.mjs +2 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
- package/runtime/scripts/lib/package-at.mjs +5 -6
- package/runtime/scripts/lib/roots.mjs +2 -2
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
- package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
- package/scaffold/app.mjs +4 -4
- package/src/main.mjs +3 -3
- package/sync/index.mjs +14 -1
- package/sync/managed.mjs +3 -2
- package/templates/app/codegen-graphql.mjs.partial +4 -4
- package/templates/app/quality-config/sonar-project.properties +4 -2
- package/runtime/engine/canonical-json.mjs +0 -14
- package/runtime/engine/db/ledger-open.mjs +0 -38
- package/runtime/engine/db/ledger-paths.mjs +0 -87
- package/runtime/engine/db/ledger.mjs +0 -1068
- package/runtime/engine/db/provision-authority.mjs +0 -76
- package/runtime/engine/db/provision-operations.mjs +0 -146
- package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
- package/runtime/scripts/agent/codex-app-server.mjs +0 -79
- package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
- package/runtime/scripts/agent/host-agents.mjs +0 -80
- package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
- package/runtime/scripts/agent/trust-launch.mjs +0 -191
- package/runtime/scripts/agent/trust.mjs +0 -523
- package/runtime/scripts/api/fs/temp-path.mjs +0 -8
- package/runtime/scripts/api/git/diff-names.mjs +0 -8
- package/runtime/scripts/api/git/head-time.mjs +0 -10
- package/runtime/scripts/api/git/index-lock.mjs +0 -78
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/runtime/scripts/api/git/merge-tree.mjs +0 -5
- package/runtime/scripts/api/git/rev-list.mjs +0 -5
- package/runtime/scripts/api/git/rev-parse.mjs +0 -13
- package/runtime/scripts/api/git/status-query.mjs +0 -5
- package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
- package/runtime/scripts/api/npm/lib.mjs +0 -32
- package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
- package/runtime/scripts/api/npm/run-npm.mjs +0 -6
- package/runtime/scripts/api/npm/view.mjs +0 -30
- package/runtime/scripts/api/npm/whoami.mjs +0 -8
- package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
- package/runtime/scripts/api/process/kill-tree.mjs +0 -22
- package/runtime/scripts/api/process/lib.mjs +0 -125
- package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
- package/runtime/scripts/api/process/process-list.mjs +0 -28
- package/runtime/scripts/api/process/system-tool.mjs +0 -37
- package/runtime/scripts/gates/canon-digest.mjs +0 -81
- package/runtime/scripts/gates/canon-pins.mjs +0 -119
- package/runtime/scripts/gates/gate-input.mjs +0 -98
- package/runtime/scripts/gates/gate.mjs +0 -636
- package/runtime/scripts/gates/grammar-dist.mjs +0 -206
- package/runtime/scripts/gates/output.mjs +0 -40
- package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
- package/runtime/scripts/gates/test-world-run.mjs +0 -252
- package/runtime/scripts/gates/type-impact.mjs +0 -277
- package/runtime/scripts/gates/type-program.mjs +0 -113
- package/runtime/scripts/guards/assigned-command.mjs +0 -12
- package/runtime/scripts/guards/call-rights.mjs +0 -47
- package/runtime/scripts/guards/command-guard.mjs +0 -468
- package/runtime/scripts/guards/command-policy.mjs +0 -342
- package/runtime/scripts/guards/critic-reach.mjs +0 -82
- package/runtime/scripts/guards/deps-guard.mjs +0 -150
- package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
- package/runtime/scripts/guards/guards-root.mjs +0 -27
- package/runtime/scripts/guards/hook-install.mjs +0 -343
- package/runtime/scripts/guards/hook-verdict.mjs +0 -12
- package/runtime/scripts/guards/install-verdict.mjs +0 -41
- package/runtime/scripts/guards/kernel-seat.mjs +0 -63
- package/runtime/scripts/guards/launch-verdict.mjs +0 -74
- package/runtime/scripts/guards/op-starci.mjs +0 -16
- package/runtime/scripts/guards/op-write-scope.mjs +0 -27
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
- package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
- package/runtime/scripts/guards/protected-zone.mjs +0 -56
- package/runtime/scripts/guards/refusals.mjs +0 -27
- package/runtime/scripts/guards/rights.mjs +0 -268
- package/runtime/scripts/guards/seat-tools.mjs +0 -47
- package/runtime/scripts/guards/shell-commands.mjs +0 -328
- package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
- package/runtime/scripts/guards/verdict-process.mjs +0 -81
- package/runtime/scripts/lib/app-data.mjs +0 -11
- package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
- package/runtime/scripts/lib/check-format.mjs +0 -7
- package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
- package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
- package/runtime/scripts/lib/git-dir.mjs +0 -120
- package/runtime/scripts/lib/guard-command.mjs +0 -24
- package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
- package/runtime/scripts/lib/json-formats.mjs +0 -34
- package/runtime/scripts/lib/managed-json.mjs +0 -51
- package/runtime/scripts/lib/menu-parts.mjs +0 -11
- package/runtime/scripts/lib/op-gate.mjs +0 -15
- package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
- package/runtime/scripts/lib/release-version.mjs +0 -134
- package/runtime/scripts/lib/runtime-location.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
- package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
- package/runtime/scripts/lib/seat-env.mjs +0 -43
- package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
- package/runtime/scripts/lib/source-package-imports.mjs +0 -111
- package/runtime/scripts/lib/tar-files.mjs +0 -39
- package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
- package/runtime/scripts/lib/worktree-record.mjs +0 -14
- package/runtime/scripts/machine/ask-receipts.mjs +0 -113
- package/runtime/scripts/machine/evidence-store.mjs +0 -223
- package/runtime/scripts/machine/home.mjs +0 -216
- package/runtime/scripts/machine/host-lock.mjs +0 -184
- package/runtime/scripts/machine/lock-recovery.mjs +0 -55
- package/runtime/scripts/machine/roles-contract.mjs +0 -90
- package/runtime/scripts/machine/runtime-change.mjs +0 -20
- package/runtime/scripts/machine/workflow-tree.mjs +0 -122
- package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
- package/runtime/scripts/reconciler/guard-row.mjs +0 -25
- package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
- package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
- package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
- package/runtime/scripts/work/brand/brand.mjs +0 -892
- package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
- package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
- package/runtime/scripts/work/draw/draw-source.mjs +0 -412
- package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
- package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
- package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
- package/runtime/scripts/work/draw-grammar.mjs +0 -175
- package/runtime/scripts/work/draw-render-page.mjs +0 -188
- package/runtime/scripts/work/draw-render.mjs +0 -626
- package/runtime/scripts/work/png.mjs +0 -237
- package/runtime/scripts/work/render-tools.mjs +0 -64
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Read-only joins of a native dispatch and its accepted goal grant; no prompt text grants authority.
|
|
2
|
-
import { canonicalJSON } from '../canonical-json.mjs';
|
|
3
|
-
import { sha256 } from '../digest.mjs';
|
|
4
|
-
import { byCodeUnit } from '../by-code-unit.mjs';
|
|
5
|
-
import { pathKey } from '../../scripts/lib/path-key.mjs';
|
|
6
|
-
|
|
7
|
-
const denied = reason => ({ ok: false, reason });
|
|
8
|
-
const parse = text => { try { return JSON.parse(text); } catch { return null; } };
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Verify a provider-plan grant against the exact live job, latest attempt, filed admission and
|
|
12
|
-
* current owner-approved goal. The native caller-binding owner supplies `caller`; argv does not.
|
|
13
|
-
* This verifies retained conversation acceptance, not cryptographic human identity.
|
|
14
|
-
*/
|
|
15
|
-
export function provisionGoalGrant(db, { caller, operationId, planSha, ledgerId, repoRoot, worktree }) {
|
|
16
|
-
const guard = caller?.guard;
|
|
17
|
-
if (!guard || caller.seat || guard.schema !== 'starci/op-guard@1' || guard.role !== 'op'
|
|
18
|
-
|| caller.classification?.role !== 'op' || caller.classification.jobId !== guard.jobId
|
|
19
|
-
|| caller.classification.workflowId !== guard.workflowId
|
|
20
|
-
|| guard.workflowId === 'supervisor' || guard.terminal !== caller.terminal
|
|
21
|
-
|| !guard.jobId || !guard.op || !guard.ledgerRepo || !guard.workflowWorktree
|
|
22
|
-
|| pathKey(guard.ledgerRepo) !== pathKey(repoRoot) || pathKey(guard.workflowWorktree) !== pathKey(worktree)
|
|
23
|
-
|| (caller.ledgerId !== undefined && caller.ledgerId !== ledgerId)
|
|
24
|
-
|| (caller.workflowId !== undefined && caller.workflowId !== guard.workflowId))
|
|
25
|
-
return denied('provision-native-caller-unverified');
|
|
26
|
-
const job = db.prepare('SELECT * FROM jobs WHERE job_id=?').get(guard.jobId);
|
|
27
|
-
const attempt = db.prepare('SELECT * FROM op_attempts WHERE job_id=? ORDER BY attempt_id DESC LIMIT 1').get(guard.jobId);
|
|
28
|
-
const attemptId = attempt?.attempt_id;
|
|
29
|
-
if (!Number.isSafeInteger(attemptId) || attemptId < 1
|
|
30
|
-
|| (caller.attemptId !== undefined && Number(caller.attemptId) !== attemptId)
|
|
31
|
-
|| (caller.dispatchId !== undefined && caller.dispatchId !== attempt.dispatch_id)) return denied('provision-native-attempt-unverified');
|
|
32
|
-
if (job?.kind !== 'op' || job.status !== 'running' || job.workflow_id !== guard.workflowId || job.op_id !== guard.op
|
|
33
|
-
|| !attempt || attempt.job_id !== guard.jobId || attempt.workflow_id !== guard.workflowId
|
|
34
|
-
|| attempt.op_id !== guard.op || !attempt.dispatch_id || attempt.terminal_handle !== caller.terminal
|
|
35
|
-
|| attempt.managed !== 1 || attempt.started_at === null || attempt.attested_at === null
|
|
36
|
-
|| attempt.reported_at !== null || attempt.settled_at !== null || attempt.released_at !== null || attempt.end_state !== null
|
|
37
|
-
|| !attempt.worktree_path || pathKey(attempt.worktree_path) !== pathKey(worktree))
|
|
38
|
-
return denied('provision-native-attempt-stale');
|
|
39
|
-
const workflow = db.prepare('SELECT * FROM workflows WHERE workflow_id=?').get(guard.workflowId);
|
|
40
|
-
const goal = db.prepare('SELECT * FROM goals WHERE workflow_id=? ORDER BY revision DESC LIMIT 1').get(guard.workflowId);
|
|
41
|
-
const unit = db.prepare('SELECT * FROM work_units WHERE workflow_id=? AND unit_id=?').get(guard.workflowId, job.unit_id);
|
|
42
|
-
const goalJson = parse(goal?.json), payload = parse(job.payload_json);
|
|
43
|
-
if (workflow?.phase !== 'running' || workflow.archived_at !== null || goal?.approved_by !== 'owner'
|
|
44
|
-
|| !goal.markdown?.trim() || !goal.goal_identity || workflow.goal_identity !== goal.goal_identity || !goalJson || goalJson.provisional === true
|
|
45
|
-
|| !unit || unit.goal_revision !== goal.revision || unit.current_job_id !== job.job_id
|
|
46
|
-
|| payload?.goal_binding?.revision !== goal.revision || payload?.goal_binding?.identity !== goal.goal_identity)
|
|
47
|
-
return denied('provision-goal-grant-stale');
|
|
48
|
-
const contract = db.prepare('SELECT * FROM contracts WHERE attempt_id=?').get(attemptId);
|
|
49
|
-
const context = parse(contract?.context_json), packet = context?.packet;
|
|
50
|
-
if (contract?.workflow_id !== guard.workflowId || contract.job_id !== job.job_id || !contract.markdown?.trim()
|
|
51
|
-
|| packet?.op !== guard.op || packet.context?.workflow?.id !== guard.workflowId
|
|
52
|
-
|| packet.context?.workflow?.goal_revision !== goal.revision || packet.context?.workflow?.goal_identity !== goal.goal_identity
|
|
53
|
-
|| context.contract?.schema !== 'starci/contract-version@1' || context.contract?.op !== guard.op
|
|
54
|
-
|| !/^[a-f0-9]{64}$/u.test(context.contract?.digest ?? '') || !context.worktree || pathKey(context.worktree) !== pathKey(worktree)
|
|
55
|
-
|| !job.lease_token || context.lease?.token !== job.lease_token || !context.lease?.fencing || typeof context.lease.fencing !== 'object' || Array.isArray(context.lease.fencing))
|
|
56
|
-
return denied('provision-filed-contract-unverified');
|
|
57
|
-
const leases = db.prepare('SELECT resource_key,generation,token,expires_at FROM leases WHERE job_id=?').all(job.job_id);
|
|
58
|
-
const fencing = context.lease.fencing;
|
|
59
|
-
if (Object.keys(fencing).length !== leases.length || leases.some(lease => fencing[lease.resource_key] !== job.generation
|
|
60
|
-
|| lease.generation !== job.generation || lease.token !== job.lease_token || lease.expires_at <= Date.now()))
|
|
61
|
-
return denied('provision-filed-contract-unverified');
|
|
62
|
-
const grants = goalJson.provisionGrants, approval = goalJson.provisionGrantsApproval;
|
|
63
|
-
if (!Array.isArray(grants) || new Set(grants.map(grant => grant?.operation)).size !== grants.length || grants.some(grant => !grant || Object.keys(grant).sort(byCodeUnit).join(',') !== 'digest,op,operation'
|
|
64
|
-
|| typeof grant.op !== 'string' || typeof grant.operation !== 'string' || !/^[a-f0-9]{64}$/u.test(grant.digest ?? '')))
|
|
65
|
-
return denied('provision-goal-grant-unsupported');
|
|
66
|
-
if (approval?.source !== 'owner-goal-entry' || approval.digest !== sha256(canonicalJSON(grants))
|
|
67
|
-
|| approval.assurance !== 'conversation-context-not-authenticated') return denied('provision-goal-grant-unaccepted');
|
|
68
|
-
if (goal.revision === 0 ? goal.approval_ref !== approval.digest
|
|
69
|
-
: goal.approval_ref !== goalJson.revision?.approvalToken || !/^rev-[a-f0-9]{64}$/u.test(goal.approval_ref ?? ''))
|
|
70
|
-
return denied('provision-goal-grant-unaccepted');
|
|
71
|
-
const matches = grants.filter(grant => grant.op === guard.op && grant.operation === operationId && grant.digest === planSha);
|
|
72
|
-
if (matches.length !== 1) return denied('provision-goal-grant-missing');
|
|
73
|
-
return { ok: true, kind: 'owner-goal', workflow: guard.workflowId, goalRevision: goal.revision,
|
|
74
|
-
goalIdentity: goal.goal_identity, op: guard.op, job: job.job_id, attempt: attemptId,
|
|
75
|
-
dispatch: attempt.dispatch_id, operation: operationId, digest: planSha, assurance: approval.assurance };
|
|
76
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
// Standalone operation writes are reached only through the ledger writer's typed namespace.
|
|
2
|
-
import { sha256 } from '../digest.mjs';
|
|
3
|
-
import { provisionGoalGrant } from './provision-authority.mjs';
|
|
4
|
-
import { describeSchema, readSchemaObjects } from './machine-schema.mjs';
|
|
5
|
-
|
|
6
|
-
const failures = new WeakSet();
|
|
7
|
-
const need = (condition, code) => {
|
|
8
|
-
if (!condition) { const error = Object.assign(new Error(code), { code }); failures.add(error); throw error; }
|
|
9
|
-
};
|
|
10
|
-
/** Recognize only this writer's fixed invariant refusals; arbitrary database errors stay private. */
|
|
11
|
-
export const isProvisionOperationFailure = error => typeof error === 'object' && error !== null && failures.has(error);
|
|
12
|
-
|
|
13
|
-
const SCHEMA_FAILURE = 'STARCI_LEDGER_SCHEMA_REFUSED';
|
|
14
|
-
const SCHEMA_BEGIN = '-- starci:provision-schema:begin';
|
|
15
|
-
const SCHEMA_END = '-- starci:provision-schema:end';
|
|
16
|
-
const identifierKey = name => name.toLowerCase();
|
|
17
|
-
const provisionName = name => identifierKey(name).startsWith('provision_');
|
|
18
|
-
|
|
19
|
-
function canonicalProvisionSchema(canonicalSql) {
|
|
20
|
-
const begin = canonicalSql.indexOf(SCHEMA_BEGIN), end = canonicalSql.indexOf(SCHEMA_END);
|
|
21
|
-
need(begin >= 0 && end > begin && canonicalSql.lastIndexOf(SCHEMA_BEGIN) === begin
|
|
22
|
-
&& canonicalSql.lastIndexOf(SCHEMA_END) === end, SCHEMA_FAILURE);
|
|
23
|
-
const canonical = describeSchema(canonicalSql);
|
|
24
|
-
const objects = canonical.objects.filter(row => provisionName(row.name));
|
|
25
|
-
const names = new Set(objects.map(row => identifierKey(row.name)));
|
|
26
|
-
const tables = new Set(objects.filter(row => row.type === 'table').map(row => identifierKey(row.name)));
|
|
27
|
-
const blobRefs = canonical.blobRefs.filter(row => tables.has(identifierKey(row.table_name)));
|
|
28
|
-
need(objects.length > 0 && tables.size > 0 && blobRefs.length > 0, SCHEMA_FAILURE);
|
|
29
|
-
return { sql: canonicalSql.slice(begin + SCHEMA_BEGIN.length, end), objects, names, tables, blobRefs };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function provisionSchemaState(db, schema) {
|
|
33
|
-
const all = readSchemaObjects(db);
|
|
34
|
-
need(['blobs', 'blob_ref_columns'].every(name => all.some(row => row.type === 'table' && row.name === name)), SCHEMA_FAILURE);
|
|
35
|
-
return {
|
|
36
|
-
objects: all.filter(row => provisionName(row.name) || schema.names.has(identifierKey(row.name)) || schema.tables.has(identifierKey(row.tbl_name))),
|
|
37
|
-
blobRefs: db.prepare('SELECT table_name,column_name FROM blob_ref_columns ORDER BY table_name,column_name').all()
|
|
38
|
-
.filter(row => provisionName(row.table_name) || schema.tables.has(identifierKey(row.table_name))),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function exactProvisionSchema(actual, expected) {
|
|
43
|
-
const objects = rows => JSON.stringify(rows.map(row => [row.type, row.name, row.tbl_name, row.identity]));
|
|
44
|
-
return objects(actual.objects) === objects(expected.objects)
|
|
45
|
-
&& JSON.stringify(actual.blobRefs) === JSON.stringify(expected.blobRefs);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Install only a wholly absent canonical domain inside the ledger's fenced write transaction. */
|
|
49
|
-
function ensureProvisionSchema(db, schema) {
|
|
50
|
-
const before = provisionSchemaState(db, schema);
|
|
51
|
-
if (before.objects.length || before.blobRefs.length) {
|
|
52
|
-
need(exactProvisionSchema(before, schema), SCHEMA_FAILURE);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
db.exec(schema.sql);
|
|
56
|
-
need(exactProvisionSchema(provisionSchemaState(db, schema), schema), SCHEMA_FAILURE);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Read one provider operation; older compatible ledgers without this additive store return null. */
|
|
60
|
-
export function provisionOperation(db, operationId) {
|
|
61
|
-
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='provision_operations'").get()) return null;
|
|
62
|
-
return db.prepare('SELECT * FROM provision_operations WHERE operation_id=?').get(operationId) ?? null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Prior completed operations bind the exact public plan, including credential references and destination. */
|
|
66
|
-
export function priorProvisionOperations(db, { planSha, excludeId }) {
|
|
67
|
-
return db.prepare("SELECT * FROM provision_operations WHERE plan_sha=? AND operation_id<>? AND state IN ('applied','verified') ORDER BY updated_at DESC LIMIT 1")
|
|
68
|
-
.all(planSha, excludeId);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function journal(db, row, kind, at) {
|
|
72
|
-
const prior = db.prepare('SELECT digest FROM provision_events WHERE operation_id=? ORDER BY seq DESC LIMIT 1').get(row.operation_id)?.digest ?? null;
|
|
73
|
-
const digest = sha256(JSON.stringify([prior, row.operation_id, kind, row.state, row.receipt_sha, at]));
|
|
74
|
-
db.prepare('INSERT INTO provision_events(operation_id,kind,state,receipt_sha,prev_digest,digest,created_at) VALUES(?,?,?,?,?,?,?)')
|
|
75
|
-
.run(row.operation_id, kind, row.state, row.receipt_sha, prior, digest, at);
|
|
76
|
-
return row;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Prepare immutable intent. A reused identifier must retain the exact original plan and target. */
|
|
80
|
-
function prepareProvisionOperation(db, { operationId, action, targetId, transport, endpointIdentity, coordinationKey, planSha, at = Date.now() }) {
|
|
81
|
-
need(typeof operationId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9-]{0,127}$/u.test(operationId), 'provision-operation-id-invalid');
|
|
82
|
-
const previous = provisionOperation(db, operationId);
|
|
83
|
-
if (previous) {
|
|
84
|
-
need(previous.action === action && previous.transport === transport && previous.endpoint_identity === endpointIdentity
|
|
85
|
-
&& previous.coordination_key === coordinationKey && previous.plan_sha === planSha, 'provision-operation-intent-conflict');
|
|
86
|
-
return previous;
|
|
87
|
-
}
|
|
88
|
-
db.prepare("INSERT INTO provision_operations(operation_id,action,target_id,transport,endpoint_identity,coordination_key,plan_sha,state,code,created_at,updated_at) VALUES(?,?,?,?,?,?,?,'planned','plan-prepared',?,?)")
|
|
89
|
-
.run(operationId, action, targetId, transport, endpointIdentity, coordinationKey, planSha, at, at);
|
|
90
|
-
return journal(db, provisionOperation(db, operationId), 'prepared', at);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Record the native terminal's scoped acceptance; a digest flag alone never reaches this writer. */
|
|
94
|
-
function acceptProvisionOperation(db, { operationId, planSha, at = Date.now() }) {
|
|
95
|
-
const row = provisionOperation(db, operationId);
|
|
96
|
-
need(row && row.plan_sha === planSha, 'provision-plan-digest-mismatch');
|
|
97
|
-
need(['planned', 'accepted', 'unknown', 'partial'].includes(row.state), 'provision-accept-state-refused');
|
|
98
|
-
if (row.accepted_by === 'owner-terminal') return row;
|
|
99
|
-
db.prepare("UPDATE provision_operations SET state=CASE WHEN state='planned' THEN 'accepted' ELSE state END,accepted_by='owner-terminal',approval_ref=?,accepted_at=?,updated_at=?,code='owner-plan-accepted' WHERE operation_id=?")
|
|
100
|
-
.run(planSha, at, at, operationId);
|
|
101
|
-
return journal(db, provisionOperation(db, operationId), 'accepted', at);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Consume an exact accepted goal grant after the native binding check, rejoining current rows atomically. */
|
|
105
|
-
function authorizeProvisionGrant(db, { operationId, planSha, caller, ledgerId, repoRoot, worktree, authoritySha, at = Date.now() }) {
|
|
106
|
-
const authority = provisionGoalGrant(db, { operationId, planSha, caller, ledgerId, repoRoot, worktree });
|
|
107
|
-
need(authority.ok, 'provision-goal-grant-refused');
|
|
108
|
-
const row = provisionOperation(db, operationId);
|
|
109
|
-
need(row && row.plan_sha === planSha, 'provision-plan-digest-mismatch');
|
|
110
|
-
db.prepare("UPDATE provision_operations SET state=CASE WHEN state='planned' THEN 'accepted' ELSE state END,accepted_by='owner-goal',approval_ref=?,accepted_at=COALESCE(accepted_at,?),updated_at=? WHERE operation_id=?")
|
|
111
|
-
.run(authoritySha, at, at, operationId);
|
|
112
|
-
return journal(db, provisionOperation(db, operationId), 'goal-authorized', at);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Atomically claim one accepted execution or explicit unknown-outcome reconciliation. */
|
|
116
|
-
function claimProvisionOperation(db, { operationId, planSha, reconcile = false, at = Date.now(), pid = process.pid }) {
|
|
117
|
-
const row = provisionOperation(db, operationId);
|
|
118
|
-
need(row && row.plan_sha === planSha && row.accepted_at !== null, 'provision-acceptance-required');
|
|
119
|
-
need(reconcile ? ['unknown', 'partial', 'running'].includes(row.state) : row.state === 'accepted', 'provision-reconcile-required');
|
|
120
|
-
db.prepare("UPDATE provision_operations SET state='running',owner_pid=?,generation=generation+1,updated_at=?,code=? WHERE operation_id=?")
|
|
121
|
-
.run(pid, at, reconcile ? 'reconciliation-started' : 'execution-started', operationId);
|
|
122
|
-
return journal(db, provisionOperation(db, operationId), reconcile ? 'reconciling' : 'started', at);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Persist safe checkpoints and terminal receipts under the execution generation that owns them. */
|
|
126
|
-
function updateProvisionOperation(db, { operationId, generation, state = 'running', receiptSha = null, metadataSha = null, writeAck = false, code, at = Date.now(), pid = process.pid }) {
|
|
127
|
-
const row = provisionOperation(db, operationId);
|
|
128
|
-
need(row?.state === 'running' && row.generation === generation && row.owner_pid === pid, 'provision-executor-stale');
|
|
129
|
-
need(['running', 'applied', 'verified', 'failed', 'unknown', 'partial'].includes(state), 'provision-state-invalid');
|
|
130
|
-
db.prepare('UPDATE provision_operations SET state=?,receipt_sha=COALESCE(?,receipt_sha),metadata_sha=COALESCE(?,metadata_sha),write_ack=MAX(write_ack,?),code=?,updated_at=? WHERE operation_id=?')
|
|
131
|
-
.run(state, receiptSha, metadataSha, writeAck ? 1 : 0, code, at, operationId);
|
|
132
|
-
return journal(db, provisionOperation(db, operationId), state === 'running' ? 'checkpoint' : 'finished', at);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Canonical schema text is injected once; schema work stays lazy inside the existing typed transaction. */
|
|
136
|
-
export function provisionWrites(canonicalSql) {
|
|
137
|
-
let schema;
|
|
138
|
-
return Object.freeze({
|
|
139
|
-
prepareProvisionOperation(db, args) {
|
|
140
|
-
schema ??= canonicalProvisionSchema(canonicalSql);
|
|
141
|
-
ensureProvisionSchema(db, schema);
|
|
142
|
-
return prepareProvisionOperation(db, args);
|
|
143
|
-
},
|
|
144
|
-
acceptProvisionOperation, authorizeProvisionGrant, claimProvisionOperation, updateProvisionOperation,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto"
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path"
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The build stamp ties a built `dist/` to the exact source it was built from.
|
|
8
|
-
*
|
|
9
|
-
* `npm run build` ends by writing `dist/.build-stamp.json`: the package version, the source digest
|
|
10
|
-
* (below), a digest of the built files, and the build time. The root check
|
|
11
|
-
* `scripts/checks/check-grammar-dist.mjs` recomputes the source digest and refuses a dist whose stamp is
|
|
12
|
-
* missing or names other source, so a stale build can no longer pass for the current one.
|
|
13
|
-
*
|
|
14
|
-
* The source digest covers everything that decides what `dist/` contains: every file under `src/`
|
|
15
|
-
* except stories, test helpers, specs and tests (they never ship), `tsconfig.build.json`,
|
|
16
|
-
* `packages/grammar/scripts/copy-css.mjs`, and the `exports` and `files` fields of `package.json`. Line endings are
|
|
17
|
-
* normalised so a CRLF checkout of the same commit has the same digest.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export const STAMP_FILE = ".build-stamp.json"
|
|
21
|
-
export const STAMP_SCHEMA = "starci/grammar-build-stamp@1"
|
|
22
|
-
export const DIGEST_ALGORITHM = "starci/grammar-source-digest@1"
|
|
23
|
-
|
|
24
|
-
const DEV_ONLY_DIRS = new Set(["stories", "__test__"])
|
|
25
|
-
const DEV_ONLY_FILE = /\.(?:stories|spec|test)\./
|
|
26
|
-
|
|
27
|
-
const slash = (value) => value.replaceAll("\\", "/")
|
|
28
|
-
const normalised = (buffer) => Buffer.from(buffer.toString("latin1").replaceAll("\r\n", "\n"), "latin1")
|
|
29
|
-
|
|
30
|
-
function filesUnder(dir, keep = () => true) {
|
|
31
|
-
if (!existsSync(dir)) return []
|
|
32
|
-
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
33
|
-
const full = join(dir, entry.name)
|
|
34
|
-
if (entry.isDirectory()) return keep(entry, true) ? filesUnder(full, keep) : []
|
|
35
|
-
return entry.isFile() && keep(entry, false) ? [full] : []
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** The order `.sort()` gives without a compare function (UTF-16 code units): the stamp digest depends on it. */
|
|
40
|
-
const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
|
|
41
|
-
|
|
42
|
-
function hashEntries(entries) {
|
|
43
|
-
const hash = createHash("sha256")
|
|
44
|
-
for (const [name, bytes] of entries.sort(([a], [b]) => byCodeUnit(a, b))) {
|
|
45
|
-
hash.update(name)
|
|
46
|
-
hash.update("\0")
|
|
47
|
-
hash.update(bytes === null ? "<absent>" : bytes)
|
|
48
|
-
hash.update("\0")
|
|
49
|
-
}
|
|
50
|
-
return `sha256:${hash.digest("hex")}`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The files the source digest reads, relative to the package root, in digest order. */
|
|
54
|
-
export function sourceInputs(packageRoot) {
|
|
55
|
-
const src = filesUnder(join(packageRoot, "src"), (entry, isDir) =>
|
|
56
|
-
isDir ? !DEV_ONLY_DIRS.has(entry.name) : !DEV_ONLY_FILE.test(entry.name))
|
|
57
|
-
return [...src.map((file) => slash(relative(packageRoot, file))), "tsconfig.build.json", "scripts/copy-css.mjs"].sort(byCodeUnit)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The digest of everything that decides the content of `dist/`. */
|
|
61
|
-
export function sourceDigest(packageRoot) {
|
|
62
|
-
const entries = sourceInputs(packageRoot).map((name) => {
|
|
63
|
-
const file = join(packageRoot, name)
|
|
64
|
-
return [name, existsSync(file) ? normalised(readFileSync(file)) : null]
|
|
65
|
-
})
|
|
66
|
-
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"))
|
|
67
|
-
entries.push(["package.json#exports+files", Buffer.from(JSON.stringify({ exports: manifest.exports ?? null, files: manifest.files ?? null }))])
|
|
68
|
-
return hashEntries(entries)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** The digest of the built files themselves (everything in `dist/` except the stamp). */
|
|
72
|
-
export function distDigest(packageRoot) {
|
|
73
|
-
const dist = join(packageRoot, "dist")
|
|
74
|
-
const files = filesUnder(dist).map((file) => slash(relative(dist, file))).filter((name) => name !== STAMP_FILE)
|
|
75
|
-
return hashEntries(files.map((name) => [name, normalised(readFileSync(join(dist, name)))]))
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function writeStamp(packageRoot, { now = new Date() } = {}) {
|
|
79
|
-
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"))
|
|
80
|
-
if (!existsSync(join(packageRoot, "dist"))) throw new Error("Cannot stamp a build that has no dist/ directory.")
|
|
81
|
-
const stamp = {
|
|
82
|
-
schema: STAMP_SCHEMA,
|
|
83
|
-
package: manifest.name,
|
|
84
|
-
version: manifest.version,
|
|
85
|
-
algorithm: DIGEST_ALGORITHM,
|
|
86
|
-
sourceDigest: sourceDigest(packageRoot),
|
|
87
|
-
distDigest: distDigest(packageRoot),
|
|
88
|
-
builtAt: now.toISOString(),
|
|
89
|
-
}
|
|
90
|
-
writeFileSync(join(packageRoot, "dist", STAMP_FILE), `${JSON.stringify(stamp, null, 2)}\n`)
|
|
91
|
-
return stamp
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
95
|
-
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
96
|
-
const stamp = writeStamp(packageRoot)
|
|
97
|
-
console.log(`dist/${STAMP_FILE}: ${stamp.package}@${stamp.version} ${stamp.sourceDigest.slice(0, 19)}`)
|
|
98
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// scripts/agent/codex-app-server.mjs — the runtime's `codex app-server` client (kept out of trust.mjs, which is over
|
|
2
|
-
// its size budget).
|
|
3
|
-
//
|
|
4
|
-
// Codex computes the hash it trusts a hook by; launch trust asks it through the app-server protocol the way Orca
|
|
5
|
-
// trusts its own hooks (scripts/agent/trust.mjs trustCodexToolGuard). The client is a node one-shot: it spawns
|
|
6
|
-
// `codex app-server`, sends initialize then each request, and prints one stdout line — the results array, or
|
|
7
|
-
// {failed} when codex ended (or never started) before answering. Without that line an early exit only drained the
|
|
8
|
-
// client's event loop: exit 0, nothing printed, and codex's stderr discarded — so "answered nothing (exit 0)" was
|
|
9
|
-
// the client's exit, never codex's. The failure names codex's exit/signal, the request it left unanswered, its
|
|
10
|
-
// stderr tail, `codex --version` and the CODEX_HOME it ran with; its text starts with APP_SERVER_FAILURE, which
|
|
11
|
-
// scripts/agent/provider-outage.mjs reads as a worker-start strike of the codex circuit.
|
|
12
|
-
//
|
|
13
|
-
// The probe is a runtime-owned child: it gets the caller's env without the seat identity or the seat's guard-shim
|
|
14
|
-
// PATH (scripts/lib/seat-env.mjs). A launch-trust call made under a Kernel seat otherwise inherits both — and
|
|
15
|
-
// `codex`'s own launcher resolves `node` through PATH, so the seat's tool wrapper bound the seat's role to it and
|
|
16
|
-
// refused the probe as a raw tool call (RIGHTS_RAW_TOOL, exit 2; `codex --version` printed the same refusal).
|
|
17
|
-
import { runNode } from '../api/node/run-node.mjs';
|
|
18
|
-
import { parseJson } from '../lib/json.mjs';
|
|
19
|
-
import { withoutSeatEnv, withoutSeatShim } from '../lib/seat-env.mjs';
|
|
20
|
-
|
|
21
|
-
// Every launch-trust app-server failure starts with this text; provider-outage.mjs classifies it.
|
|
22
|
-
export const APP_SERVER_FAILURE = 'codex app-server answered nothing';
|
|
23
|
-
|
|
24
|
-
const APP_SERVER_CLIENT = String.raw`
|
|
25
|
-
const { spawn, spawnSync } = require('node:child_process');
|
|
26
|
-
const reqs = JSON.parse(process.argv[1]);
|
|
27
|
-
const p = spawn('codex app-server', { shell: true, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
28
|
-
const out = []; let buf = ''; let next = 0; let ready = false; let err = '';
|
|
29
|
-
const send = (o) => p.stdin.write(JSON.stringify(o) + '\n');
|
|
30
|
-
const ask = () => { if (next >= reqs.length) { console.log(JSON.stringify(out)); p.kill(); process.exit(0); } send({ id: 100 + next, ...reqs[next] }); };
|
|
31
|
-
const fail = (exit, signal, spawnError) => {
|
|
32
|
-
const v = spawnSync('codex --version', { shell: true, encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
33
|
-
const version = String(v.stdout || v.stderr || '').trim() || null;
|
|
34
|
-
console.log(JSON.stringify({ failed: { exit, signal, spawnError, stage: ready ? reqs[next].method : 'initialize', stderr: err.trim(), version } }));
|
|
35
|
-
process.exit(3);
|
|
36
|
-
};
|
|
37
|
-
p.stdin.on('error', () => {});
|
|
38
|
-
p.stderr.on('data', (d) => { err = (err + d).slice(-4000); });
|
|
39
|
-
p.stdout.on('data', (d) => { buf += d; let i; while ((i = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 1); let m; try { m = JSON.parse(line); } catch { continue; }
|
|
40
|
-
if (m.id === 1) { ready = true; send({ method: 'initialized' }); ask(); }
|
|
41
|
-
else if (m.id === 100 + next) { out.push(m.error ? { error: m.error } : m.result); next += 1; ask(); } } });
|
|
42
|
-
p.on('error', (e) => fail(null, null, e.message));
|
|
43
|
-
p.on('close', (code, signal) => fail(code, signal, null));
|
|
44
|
-
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'starci-launch-trust', version: '1' } } });
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
const tail = (text, n) => { const t = String(text ?? '').replace(/\s+/g, ' ').trim(); return t.length > n ? '...' + t.slice(-n) : t; };
|
|
48
|
-
|
|
49
|
-
// The error of an app-server call that returned no results: what ended it (codex's exit/signal or the client's own),
|
|
50
|
-
// codex's stderr tail and version, and the CODEX_HOME it ran with.
|
|
51
|
-
function appServerFailure({ home, timeoutMs, r, failed }) {
|
|
52
|
-
const parts = [];
|
|
53
|
-
if (failed) {
|
|
54
|
-
parts.push(
|
|
55
|
-
failed.spawnError ? 'did not start: ' + failed.spawnError
|
|
56
|
-
: 'exited ' + (failed.exit ?? 'on ' + failed.signal) + ' before answering ' + failed.stage,
|
|
57
|
-
failed.stderr ? 'stderr: ' + tail(failed.stderr, 400) : 'no stderr',
|
|
58
|
-
failed.version ? 'codex --version: ' + tail(failed.version, 120) : 'codex --version printed nothing');
|
|
59
|
-
} else {
|
|
60
|
-
parts.push('client exit ' + r.status + (r.error ? ': ' + r.error.message : ''));
|
|
61
|
-
if (r.error?.code === 'ETIMEDOUT') parts.push('no answer within ' + timeoutMs + 'ms');
|
|
62
|
-
const stderr = tail(r.stderr, 400);
|
|
63
|
-
if (stderr) parts.push('client stderr: ' + stderr);
|
|
64
|
-
}
|
|
65
|
-
parts.push('CODEX_HOME ' + home);
|
|
66
|
-
return APP_SERVER_FAILURE + ' (' + parts.join('; ') + ')';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The app-server calls of one Codex home, in order: `requests` [{method, params}] -> results[] (an error result is
|
|
71
|
-
* {error}). No results throws the precise failure (appServerFailure).
|
|
72
|
-
*/
|
|
73
|
-
export function codexAppServer({ home, requests, timeoutMs = 60_000, env = process.env }) {
|
|
74
|
-
const r = runNode(['-e', APP_SERVER_CLIENT, JSON.stringify(requests)], { timeout: timeoutMs,
|
|
75
|
-
env: { ...withoutSeatShim(withoutSeatEnv(env)), CODEX_HOME: home } });
|
|
76
|
-
const last = parseJson(String(r.stdout ?? '').trim().split(/\r?\n/).pop() ?? '', null);
|
|
77
|
-
if (Array.isArray(last)) return last;
|
|
78
|
-
throw new Error(appServerFailure({ home, timeoutMs, r, failed: last?.failed ?? null }));
|
|
79
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// scripts/agent/codex-mirror-source.mjs — the system Codex home a launch also trusts, beside Orca's managed Codex home.
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The system Codex home (<home>/.codex). Orca starts a Codex worker in one of two homes and the launch cannot know
|
|
8
|
-
* which until Orca spawns it: the managed home (<userData>/codex-runtime-home/home), whose config.toml Orca rebuilds
|
|
9
|
-
* from this home's at every launch keeping only its own `[projects.*]` and `[hooks.state.*]` tables (a
|
|
10
|
-
* `[[hooks.PreToolUse]]` block written to the managed file alone is gone seconds after `worker-start`, observed
|
|
11
|
-
* 2026-10-07 18:32), or this home itself, once Codex has approved Orca's status hook in it (Orca 1.4.221 on Windows,
|
|
12
|
-
* as on macOS and Linux). Launch trust therefore records the project, the notices and the guard in both.
|
|
13
|
-
*
|
|
14
|
-
* Null when the launch's Codex home is no Orca-chosen one (an explicit CODEX_HOME): Orca neither mirrors from nor
|
|
15
|
-
* starts the worker in the system home then. A trust home (specs) re-roots the source under `<root>/system`, a
|
|
16
|
-
* directory of its own beside the managed `<root>/.codex`.
|
|
17
|
-
*/
|
|
18
|
-
function codexMirrorSource({ env = process.env } = {}) {
|
|
19
|
-
const root = env.STARCI_AGENT_TRUST_HOME || null;
|
|
20
|
-
if (root) return path.join(root, 'system', '.codex');
|
|
21
|
-
return env.CODEX_HOME ? null : path.join(os.homedir(), '.codex');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The Codex homes one launch records trust in: the active home(s) plus the system home when it has a config.toml.
|
|
26
|
-
* A system home without one stays untouched: Orca refuses a blank mirror source, and a file created for the launch
|
|
27
|
-
* alone would replace the owner's settings in the mirror and mask them in the home Orca starts the worker in.
|
|
28
|
-
*/
|
|
29
|
-
export function codexLaunchHomes({ homes, env = process.env }) {
|
|
30
|
-
const source = codexMirrorSource({ env });
|
|
31
|
-
const key = (dir) => path.resolve(dir).toLowerCase();
|
|
32
|
-
const present = source && fs.existsSync(path.join(source, 'config.toml')) && !homes.some((h) => key(h.dir) === key(source));
|
|
33
|
-
return present ? [...homes, { kind: 'system-codex-home', dir: path.resolve(source) }] : homes;
|
|
34
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// The host-agent preflight of one launch (contract change host-agent-preflight): before anything is trusted or started,
|
|
2
|
-
// the agent the route names must be usable on THIS host. Three refusals, each typed:
|
|
3
|
-
// agent-disabled-on-host Orca's own settings (orca-data.json of the active profile, settings.disabledTuiAgents) disable it;
|
|
4
|
-
// agent-binary-missing its CLI binary (the card's start.cli, or Orca's agentCmdOverrides command) is not on PATH;
|
|
5
|
-
// agent-bypass-missing Orca's settings.agentDefaultArgs for the agent lack the card's bypassFlag (an approval prompt no one answers);
|
|
6
|
-
// model-not-listed the model is no model the runtime declares for that provider (a provider that declares none, like Cursor whose
|
|
7
|
-
// models Orca lists dynamically, is not judged) (registry.yaml models + the pools' model pins);
|
|
8
|
-
// a card with no model flag (Devin) takes no model.
|
|
9
|
-
// No agent list is hard-coded: the card, Orca's settings and PATH decide. A test process reads none of them unless a spec passes
|
|
10
|
-
// its own `settingsFile` / `pathDirs` (the host's real settings must not decide a spec's verdict).
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import os from 'node:os';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { parseYaml } from '../../engine/yaml.mjs';
|
|
16
|
-
import { isSpecRun } from '../lib/env.mjs';
|
|
17
|
-
import { appDataBase } from '../lib/app-data.mjs';
|
|
18
|
-
|
|
19
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
20
|
-
const readYaml = (rel) => { try { return parseYaml(fs.readFileSync(path.join(root, rel), 'utf8')); } catch { return null; } };
|
|
21
|
-
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
22
|
-
|
|
23
|
-
/** Orca's userData folder (STARCI_ORCA_USER_DATA re-roots it, as for Orca's CODEX_HOME in agent/trust.mjs). */
|
|
24
|
-
export function orcaUserData({ env = process.env, platform = process.platform, home = os.homedir() } = {}) {
|
|
25
|
-
if (env.STARCI_ORCA_USER_DATA) return env.STARCI_ORCA_USER_DATA;
|
|
26
|
-
return path.join(appDataBase({ env, platform, home }), 'orca');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The settings file of Orca's active profile: <userData>/profiles/<activeProfileId>/orca-data.json. */
|
|
30
|
-
function orcaSettingsFile(opts = {}) {
|
|
31
|
-
const data = orcaUserData(opts);
|
|
32
|
-
const profile = readJson(path.join(data, 'orca-profile-index.json'))?.activeProfileId ?? 'local-default';
|
|
33
|
-
return path.join(data, 'profiles', profile, 'orca-data.json');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The models the runtime declares for `provider`: the registry `models` entries plus every pool's pins. */
|
|
37
|
-
function modelsOfProvider(provider) {
|
|
38
|
-
const found = new Set();
|
|
39
|
-
const registry = readYaml('modules/models/registry.yaml');
|
|
40
|
-
for (const [id, row] of Object.entries(registry?.models ?? {})) if (row?.provider === provider) found.add(id);
|
|
41
|
-
for (const pool of Object.values(registry?.pools ?? {})) {
|
|
42
|
-
if (pool?.provider !== provider) continue;
|
|
43
|
-
if (typeof pool.defaultModel === 'string') found.add(pool.defaultModel);
|
|
44
|
-
for (const v of Object.values(pool.models ?? {})) if (typeof v === 'string') found.add(v);
|
|
45
|
-
}
|
|
46
|
-
return found;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const onPath = (binary, { pathDirs, platform, env = process.env }) => {
|
|
50
|
-
const exts = platform === 'win32' ? ['', ...(env.PATHEXT || '.EXE;.CMD;.BAT').split(';').map((e) => e.toLowerCase())] : [''];
|
|
51
|
-
return pathDirs.some((dir) => exts.some((ext) => { try { return fs.statSync(path.join(dir, binary + ext)).isFile(); } catch { return false; } }));
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* {ok:true} or {ok:false, code, error}. `card` is the agent card (loadAdapter). Injectable for specs: settingsFile, pathDirs,
|
|
56
|
-
* platform, env, models (a Set replacing the declared models).
|
|
57
|
-
*/
|
|
58
|
-
export function hostAgentVerdict({ provider, model = null, card, env = process.env, platform = process.platform, settingsFile = null, pathDirs = null, models = null }) {
|
|
59
|
-
if (!settingsFile && !pathDirs && isSpecRun(env)) return { ok: true, skipped: 'a test process does not read the host agents' };
|
|
60
|
-
const agent = card?.start?.agentArgument ?? provider;
|
|
61
|
-
const settings = readJson(settingsFile ?? orcaSettingsFile({ env, platform }))?.settings ?? {};
|
|
62
|
-
if ((settings.disabledTuiAgents ?? []).includes(agent)) {
|
|
63
|
-
return { ok: false, code: 'agent-disabled-on-host', error: `agent '${agent}' is disabled in Orca's settings (disabledTuiAgents): enable it in Orca or route the role to another agent` };
|
|
64
|
-
}
|
|
65
|
-
const flag = card?.start?.bypassFlag;
|
|
66
|
-
if (flag && !String(settings.agentDefaultArgs?.[agent] ?? '').includes(flag)) {
|
|
67
|
-
return { ok: false, code: 'agent-bypass-missing', error: `agent '${agent}' has no '${flag}' in Orca's agentDefaultArgs: a worker would stop at an approval prompt no one answers; add it in Orca's settings` };
|
|
68
|
-
}
|
|
69
|
-
const override = settings.agentCmdOverrides?.[agent];
|
|
70
|
-
const binary = (typeof override === 'string' && override.trim() ? override.trim().split(/\s+/)[0] : null) ?? card?.start?.cli ?? agent;
|
|
71
|
-
const dirs = pathDirs ?? String(env.PATH || env.Path || '').split(path.delimiter).filter(Boolean);
|
|
72
|
-
if (!path.isAbsolute(binary) && !onPath(binary, { pathDirs: dirs, platform, env })) {
|
|
73
|
-
return { ok: false, code: 'agent-binary-missing', error: `agent '${agent}' needs the CLI binary '${binary}', which is not on PATH: install it or route the role to another agent` };
|
|
74
|
-
}
|
|
75
|
-
if (card?.start?.modelArgument !== false && model) {
|
|
76
|
-
const listed = models ?? modelsOfProvider(provider);
|
|
77
|
-
if (listed.size && !listed.has(model)) return { ok: false, code: 'model-not-listed', error: `model '${model}' is not a model the runtime declares for provider '${provider}' (modules/models/registry.yaml: models, pools)` };
|
|
78
|
-
}
|
|
79
|
-
return { ok: true };
|
|
80
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// Current launch-root identity and owner-profile authorization. This module
|
|
2
|
-
// resolves authority only; the trust writer keeps all provider file effects.
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { revParseQuery } from '../api/git/rev-parse-query.mjs';
|
|
6
|
-
import { launchTrustSettings } from '../../engine/config.mjs';
|
|
7
|
-
|
|
8
|
-
/** The launch cwd as a directory, or null for an Orca selector ('active', 'id:…'). */
|
|
9
|
-
function launchDirectory(worktree) {
|
|
10
|
-
if (typeof worktree !== 'string' || !worktree.trim()) return null;
|
|
11
|
-
const p = worktree.startsWith('path:') ? worktree.slice(5) : worktree;
|
|
12
|
-
try { return fs.statSync(p).isDirectory() ? path.resolve(p) : null; } catch { return null; }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Paths Codex keys trust by: the cwd, its git toplevel and the main worktree root. */
|
|
16
|
-
export function codexTrustPaths(cwd) {
|
|
17
|
-
const out = [path.resolve(cwd)];
|
|
18
|
-
const git = (...args) => {
|
|
19
|
-
try {
|
|
20
|
-
const r = revParseQuery(args, { dir: cwd, timeout: 10000 });
|
|
21
|
-
return r.status === 0 ? r.stdout.trim() : null;
|
|
22
|
-
} catch { return null; }
|
|
23
|
-
};
|
|
24
|
-
const top = git('--show-toplevel');
|
|
25
|
-
if (top) out.push(path.resolve(top));
|
|
26
|
-
const common = git('--path-format=absolute', '--git-common-dir');
|
|
27
|
-
if (common && path.basename(common) === '.git') out.push(path.resolve(path.dirname(common)));
|
|
28
|
-
const seen = new Set();
|
|
29
|
-
return out.filter((p) => {
|
|
30
|
-
const k = process.platform === 'win32' ? p.toLowerCase() : p;
|
|
31
|
-
if (seen.has(k)) return false;
|
|
32
|
-
seen.add(k);
|
|
33
|
-
return true;
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Resolve an existing checkout to exact current owner roots; never authorize by path prefix. */
|
|
38
|
-
export function launchTrustVerdict({cwd,config,platform=process.platform,roots=codexTrustPaths}={}){
|
|
39
|
-
const dir=launchDirectory(cwd);
|
|
40
|
-
if(!dir)return {ok:false,status:'declined',reason:'automatic trust requires a resolved existing launch directory'};
|
|
41
|
-
let policy;try{policy=config===undefined?launchTrustSettings():launchTrustSettings(config);}catch(error){return {ok:false,status:'declined',reason:String(error.message)};}
|
|
42
|
-
if(policy?.profile!=='automatic')return {ok:false,status:'declined',reason:policy?.profile==='declined'?'owner declined automatic launch trust':'current owner launchTrust profile is not adopted'};
|
|
43
|
-
const key=p=>{const real=fs.realpathSync(p);return platform==='win32'?real.toLowerCase():real;};
|
|
44
|
-
try{
|
|
45
|
-
const paths=roots(dir);
|
|
46
|
-
const approved=new Map(policy.roots.map(root=>[key(root),root]));
|
|
47
|
-
// A worktree is authorized by its exact Git main root. A non-Git directory
|
|
48
|
-
// needs its own exact root entry; a child of an approved directory is insufficient.
|
|
49
|
-
const root=paths.at(-1);
|
|
50
|
-
if(!root||!approved.has(key(root)))return {ok:false,status:'declined',reason:'launch repository is outside the exact owner-approved roots'};
|
|
51
|
-
return {ok:true,dir,paths,approval:{approvedBy:policy.approvedBy,approvalRef:policy.approvalRef,root:approved.get(key(root))}};
|
|
52
|
-
}catch(error){return {ok:false,status:'declined',reason:`cannot verify launch roots: ${error.message}`};}
|
|
53
|
-
}
|