@starci/hfs 4.7.47 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -9
  3. package/emit/operation-schemas.mjs +3 -0
  4. package/lint/bound-sys.cjs +2 -2
  5. package/package.json +11 -5
  6. package/runtime/engine/admission.mjs +1 -1
  7. package/runtime/engine/config.mjs +2 -2
  8. package/runtime/engine/db/machine.mjs +2 -3
  9. package/runtime/engine/invalid-config.mjs +1 -1
  10. package/runtime/knowledge/grammars/{starci → core}/playbook.yaml +3 -3
  11. package/runtime/knowledge/grammars/index.yaml +14 -23
  12. package/runtime/knowledge/grammars/offset-pop/playbook.yaml +2 -2
  13. package/runtime/knowledge/hfs/canon-pins.yaml +23 -23
  14. package/runtime/knowledge/hfs/fe-stack.yaml +1 -1
  15. package/runtime/knowledge/hfs/rules.yaml +83 -60
  16. package/runtime/modules/kernel/failure-codes.yaml +23 -13
  17. package/runtime/scripts/guards/git-policy.mjs +2 -3
  18. package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
  19. package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
  20. package/runtime/scripts/lib/example-refs.mjs +10 -3
  21. package/runtime/scripts/lib/grammar-lint.mjs +16 -3
  22. package/runtime/scripts/lib/package-at.mjs +5 -6
  23. package/runtime/scripts/lib/roots.mjs +2 -2
  24. package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
  25. package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
  26. package/scaffold/app.mjs +6 -6
  27. package/src/main.mjs +3 -3
  28. package/sync/hygiene.mjs +1 -1
  29. package/sync/index.mjs +15 -2
  30. package/sync/managed.mjs +3 -2
  31. package/templates/app/codegen-graphql.mjs.partial +4 -4
  32. package/templates/app/quality-config/sonar-project.properties +4 -2
  33. package/runtime/engine/canonical-json.mjs +0 -14
  34. package/runtime/engine/db/ledger-open.mjs +0 -38
  35. package/runtime/engine/db/ledger-paths.mjs +0 -87
  36. package/runtime/engine/db/ledger.mjs +0 -1068
  37. package/runtime/engine/db/provision-authority.mjs +0 -76
  38. package/runtime/engine/db/provision-operations.mjs +0 -146
  39. package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
  40. package/runtime/scripts/agent/codex-app-server.mjs +0 -79
  41. package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
  42. package/runtime/scripts/agent/host-agents.mjs +0 -80
  43. package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
  44. package/runtime/scripts/agent/trust-launch.mjs +0 -191
  45. package/runtime/scripts/agent/trust.mjs +0 -523
  46. package/runtime/scripts/api/fs/temp-path.mjs +0 -8
  47. package/runtime/scripts/api/git/diff-names.mjs +0 -8
  48. package/runtime/scripts/api/git/head-time.mjs +0 -10
  49. package/runtime/scripts/api/git/index-lock.mjs +0 -78
  50. package/runtime/scripts/api/git/log.mjs +0 -5
  51. package/runtime/scripts/api/git/merge-tree.mjs +0 -5
  52. package/runtime/scripts/api/git/rev-list.mjs +0 -5
  53. package/runtime/scripts/api/git/rev-parse.mjs +0 -13
  54. package/runtime/scripts/api/git/status-query.mjs +0 -5
  55. package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
  56. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  57. package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
  58. package/runtime/scripts/api/npm/lib.mjs +0 -32
  59. package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
  60. package/runtime/scripts/api/npm/run-npm.mjs +0 -6
  61. package/runtime/scripts/api/npm/view.mjs +0 -30
  62. package/runtime/scripts/api/npm/whoami.mjs +0 -8
  63. package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
  64. package/runtime/scripts/api/process/kill-tree.mjs +0 -22
  65. package/runtime/scripts/api/process/lib.mjs +0 -125
  66. package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
  67. package/runtime/scripts/api/process/process-list.mjs +0 -28
  68. package/runtime/scripts/api/process/system-tool.mjs +0 -37
  69. package/runtime/scripts/gates/canon-digest.mjs +0 -81
  70. package/runtime/scripts/gates/canon-pins.mjs +0 -119
  71. package/runtime/scripts/gates/gate-input.mjs +0 -98
  72. package/runtime/scripts/gates/gate.mjs +0 -636
  73. package/runtime/scripts/gates/grammar-dist.mjs +0 -206
  74. package/runtime/scripts/gates/output.mjs +0 -40
  75. package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
  76. package/runtime/scripts/gates/test-world-run.mjs +0 -252
  77. package/runtime/scripts/gates/type-impact.mjs +0 -277
  78. package/runtime/scripts/gates/type-program.mjs +0 -113
  79. package/runtime/scripts/guards/assigned-command.mjs +0 -12
  80. package/runtime/scripts/guards/call-rights.mjs +0 -47
  81. package/runtime/scripts/guards/command-guard.mjs +0 -468
  82. package/runtime/scripts/guards/command-policy.mjs +0 -342
  83. package/runtime/scripts/guards/critic-reach.mjs +0 -82
  84. package/runtime/scripts/guards/deps-guard.mjs +0 -150
  85. package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
  86. package/runtime/scripts/guards/guards-root.mjs +0 -27
  87. package/runtime/scripts/guards/hook-install.mjs +0 -343
  88. package/runtime/scripts/guards/hook-verdict.mjs +0 -12
  89. package/runtime/scripts/guards/install-verdict.mjs +0 -41
  90. package/runtime/scripts/guards/kernel-seat.mjs +0 -63
  91. package/runtime/scripts/guards/launch-verdict.mjs +0 -74
  92. package/runtime/scripts/guards/op-starci.mjs +0 -16
  93. package/runtime/scripts/guards/op-write-scope.mjs +0 -27
  94. package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
  95. package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
  96. package/runtime/scripts/guards/protected-zone.mjs +0 -56
  97. package/runtime/scripts/guards/refusals.mjs +0 -27
  98. package/runtime/scripts/guards/rights.mjs +0 -268
  99. package/runtime/scripts/guards/seat-tools.mjs +0 -47
  100. package/runtime/scripts/guards/shell-commands.mjs +0 -328
  101. package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
  102. package/runtime/scripts/guards/verdict-process.mjs +0 -81
  103. package/runtime/scripts/lib/app-data.mjs +0 -11
  104. package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
  105. package/runtime/scripts/lib/check-format.mjs +0 -7
  106. package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
  107. package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
  108. package/runtime/scripts/lib/git-dir.mjs +0 -120
  109. package/runtime/scripts/lib/guard-command.mjs +0 -24
  110. package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
  111. package/runtime/scripts/lib/json-formats.mjs +0 -34
  112. package/runtime/scripts/lib/managed-json.mjs +0 -51
  113. package/runtime/scripts/lib/menu-parts.mjs +0 -11
  114. package/runtime/scripts/lib/op-gate.mjs +0 -15
  115. package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
  116. package/runtime/scripts/lib/release-version.mjs +0 -134
  117. package/runtime/scripts/lib/runtime-location.mjs +0 -21
  118. package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
  119. package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
  120. package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
  121. package/runtime/scripts/lib/seat-env.mjs +0 -43
  122. package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
  123. package/runtime/scripts/lib/source-package-imports.mjs +0 -111
  124. package/runtime/scripts/lib/tar-files.mjs +0 -39
  125. package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
  126. package/runtime/scripts/lib/worktree-record.mjs +0 -14
  127. package/runtime/scripts/machine/ask-receipts.mjs +0 -113
  128. package/runtime/scripts/machine/evidence-store.mjs +0 -223
  129. package/runtime/scripts/machine/home.mjs +0 -216
  130. package/runtime/scripts/machine/host-lock.mjs +0 -184
  131. package/runtime/scripts/machine/lock-recovery.mjs +0 -55
  132. package/runtime/scripts/machine/roles-contract.mjs +0 -90
  133. package/runtime/scripts/machine/runtime-change.mjs +0 -20
  134. package/runtime/scripts/machine/workflow-tree.mjs +0 -122
  135. package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
  136. package/runtime/scripts/reconciler/guard-row.mjs +0 -25
  137. package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
  138. package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
  139. package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
  140. package/runtime/scripts/work/brand/brand.mjs +0 -892
  141. package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
  142. package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
  143. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
  144. package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
  145. package/runtime/scripts/work/draw/draw-source.mjs +0 -412
  146. package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
  147. package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
  148. package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
  149. package/runtime/scripts/work/draw-grammar.mjs +0 -175
  150. package/runtime/scripts/work/draw-render-page.mjs +0 -188
  151. package/runtime/scripts/work/draw-render.mjs +0 -626
  152. package/runtime/scripts/work/png.mjs +0 -237
  153. 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
- }