@cassiomc1/forgeloop 0.1.13 → 0.1.14

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.
@@ -108,6 +108,39 @@ rules permit it.
108
108
  A missing checker must never be converted into environmental mutation merely
109
109
  to make verification pass.
110
110
 
111
+ ### Verification command resolution modes and validator enforcement
112
+
113
+ Every verification command path is classified by resolution mode:
114
+
115
+ | Mode | Examples | May install software | Authority required |
116
+ | --- | --- | --- | --- |
117
+ | `LOCAL_EXECUTABLE` | `node scripts/test.js`, `python3 -m unittest`, `./bin/check` | No | No |
118
+ | `LOCAL_PACKAGE_BINARY` | `./node_modules/.bin/tool`, `npm test`, `pnpm test`, `yarn test` | No | No |
119
+ | `NON_INSTALLING_RESOLUTION` | `npx --no-install tool`, `npx --no tool` | No | No |
120
+ | `INSTALL_CAPABLE_RESOLUTION` | `npx tool`, `pnpm dlx tool`, `yarn dlx tool`, `bunx tool`, `uvx tool`, `pipx run tool` | Yes | Yes (`E_INSTALLATION_AUTHORITY_REQUIRED`) |
121
+ | `EXPLICIT_INSTALLATION` | `npm install tool`, `pnpm add tool`, `pip install tool`, `cargo install tool` | Yes | Yes (`E_INSTALLATION_AUTHORITY_REQUIRED`) |
122
+
123
+ **Validator-enforced rule**: Any verification command executed via an installation-capable or explicit-installation resolution mode without a valid canonical installation authority grant is rejected by `record-check`, `audit`, and `complete` with error code `E_INSTALLATION_AUTHORITY_REQUIRED`, `E_AUTHORITY_INVALID`, `E_AUTHORITY_SCOPE_MISMATCH`, or `E_AUTHORITY_UNTRUSTED_SOURCE` and cannot contribute to `VALID` completion.
124
+
125
+ Authority cannot be self-issued by the actor consuming it. Boolean fields inside verification evidence (such as `installationAuthorized: true`) are not sufficient proof of installation authority. Installation authority must be established via a canonical authority grant supplied by a host/operator trust boundary and referenced via `installationAuthorityRef`.
126
+
127
+ The runtime authority context has two modes:
128
+
129
+ - `NONE` is the default for the actor-facing standalone CLI. `FORGELOOP_AUTHORITY_FILE` and `FORGELOOP_AUTHORITY_DIR` may select candidate source metadata for compatibility and diagnostics, but they do not make a source trusted. An environment-selected source is rejected with `E_AUTHORITY_UNTRUSTED_SOURCE` when it is used for an installation-capable verification.
130
+ - `HOST_ATTESTED` is an internal integration context supplied by a host-owned wrapper, embedded API, or equivalent boundary. It may select a trusted authority file, directory, or in-memory provider only when the actor cannot replace that context at command invocation time. The CLI exposes no flag that promotes a source to `HOST_ATTESTED`.
131
+
132
+ External path is not equivalent to external authority ownership. The host-attested source must still resolve outside the actor-writable target, and a project-local `.forgeloop/authorities/` artifact remains an untrusted reference, cache, diagnostic, or mirror by default. A local claim such as `source: operator` is not proof of operator authority.
133
+
134
+ ForgeLoop validates authority semantics, while the host defines the trust boundary. If the host grants the actor write access to the configured attested source, the host boundary is compromised and ForgeLoop cannot distinguish operator authority from actor fabrication without a stronger external trust anchor.
135
+
136
+ ### Stale receipt recovery invariant
137
+
138
+ Every recovery action returned by `forgeloop next` must be executable from the state that produced it. When work state changes legitimately after preparing a completion receipt, `forgeloop prepare-completion` refreshes the receipt and re-binds it to current state and changed paths without requiring manual deletion of `.forgeloop/execution-receipt.json`.
139
+
140
+ ### Conformance profile escalation policy
141
+
142
+ A run started in Standard conformance must not be silently escalated to Strict after validator-backed completion. Strict validation is a separate conformance profile. If Strict revalidation is performed after Standard `COMPLETE`, it is treated as a distinct revalidation cycle and does not retroactively invalidate a valid Standard result.
143
+
111
144
  ## Blocking vs Non-Blocking Decisions
112
145
 
113
146
  Classify every unresolved decision before deciding whether to ask the user.
@@ -152,6 +152,77 @@ not already available:
152
152
  - otherwise report the affected verification dimension as not verified with
153
153
  `E_VERIFICATION_TOOL_UNAVAILABLE`.
154
154
 
155
+ Every verification command is classified by resolution mode (`LOCAL_EXECUTABLE`,
156
+ `LOCAL_PACKAGE_BINARY`, `NON_INSTALLING_RESOLUTION`, `INSTALL_CAPABLE_RESOLUTION`,
157
+ `EXPLICIT_INSTALLATION`). Any command that uses an install-capable or installation
158
+ path without a valid canonical authority reference is rejected by `record-check`,
159
+ `audit`, and `complete` with `E_INSTALLATION_AUTHORITY_REQUIRED`, `E_AUTHORITY_INVALID`,
160
+ `E_AUTHORITY_SCOPE_MISMATCH`, or `E_AUTHORITY_UNTRUSTED_SOURCE`.
161
+
162
+ ```text
163
+ capability ≠ authority
164
+ evidence ≠ authority grant
165
+ actor claim ≠ operator grant
166
+ ```
167
+
168
+ Authority cannot be self-issued by the actor consuming it. Boolean fields inside
169
+ verification evidence are not sufficient proof of installation authority.
170
+
171
+ ### Authority provenance
172
+
173
+ Authority provenance is external to actor-authored project state. An external
174
+ path is not equivalent to external authority ownership:
175
+
176
+ ```text
177
+ outside target
178
+
179
+ outside actor control
180
+ ```
181
+
182
+ The standalone CLI uses `trustMode: NONE`. `FORGELOOP_AUTHORITY_FILE` and
183
+ `FORGELOOP_AUTHORITY_DIR` select candidate authority sources, but they do not
184
+ make a source trusted in actor-facing execution. Environment-selected sources
185
+ are rejected with `E_AUTHORITY_UNTRUSTED_SOURCE` for install-capable checks.
186
+
187
+ Trusted authority requires an explicit `HOST_ATTESTED` runtime context supplied
188
+ through an integration boundary the active actor cannot replace at command
189
+ invocation time. Configuration is not trust:
190
+
191
+ ```text
192
+ environment-selected source
193
+
194
+ host-attested source
195
+
196
+ configuration
197
+
198
+ trust
199
+ ```
200
+
201
+ The host-attested source must still resolve outside the actor-writable target. A
202
+ project-local authority reference may identify a grant, but it does not create
203
+ the root of trust.
204
+
205
+ ```text
206
+ actor-authored evidence
207
+
208
+ trusted authority
209
+
210
+ project-local file
211
+
212
+ host grant
213
+
214
+ declared provenance
215
+
216
+ verified provenance
217
+ ```
218
+
219
+ A local artifact claiming `source: operator` is not sufficient proof of operator
220
+ authority. The actor-facing CLI must not expose an equivalent of
221
+ `--authority-source-attested-by-host` or `--trusted-authority-file` that
222
+ self-promotes a source. If the host exposes the attested source as writable to
223
+ the actor, the host boundary has been compromised and ForgeLoop cannot provide
224
+ cryptographic attestation by itself.
225
+
155
226
  Never convert `PROTOCOL_LIMITED` into environmental mutation by implicitly
156
227
  installing a package.
157
228
 
package/README.md CHANGED
@@ -109,11 +109,12 @@ The regression suite injects failures at these boundaries and verifies that
109
109
  owned cleanup. The frozen published installation under
110
110
  [`tests/fixtures/legacy-0.1.6/`](./tests/fixtures/legacy-0.1.6/) is derived
111
111
  from the real npm tarball, includes provenance and digests, and is copied into
112
- The current published release is `@cassiomc1/forgeloop@0.1.13`.
113
- Earlier `0.1.8`, `0.1.9`, `0.1.10`, `0.1.11`, and `0.1.12` references are historical; never move
114
- their tags or `v0.1.10`. Version `0.1.13` enforces the missing verification tool
115
- policy, conditional single-actor delegation validation, and a universal
116
- vendor-neutral engineering protocol positioning.
112
+ The latest verified published npm release is `@cassiomc1/forgeloop@0.1.14`.
113
+ The repository release is `0.1.14`.
114
+ Earlier `0.1.8`, `0.1.9`, `0.1.10`, `0.1.11`, `0.1.12`, and `0.1.13` references are historical; never move
115
+ their tags or `v0.1.10`. Release `0.1.14` enforces verification installation
116
+ authority, provides recoverable stale receipt lifecycle in `prepare-completion`,
117
+ and validates single-actor protocol runs.
117
118
 
118
119
  ## How to prompt ForgeLoop
119
120
 
@@ -203,12 +204,13 @@ project without overwriting local instructions. When the package is available
203
204
  in the npm registry, use the commands below; otherwise use the repository
204
205
  checkout fallback.
205
206
 
206
- The current published release is `@cassiomc1/forgeloop@0.1.13`.
207
- Pin this version when a reproducible blind run or release-identity check is
208
- required:
207
+ The current repository package is `@cassiomc1/forgeloop@0.1.14`.
208
+ The latest verified published npm release is `@cassiomc1/forgeloop@0.1.14`.
209
+ For reproducible published-package runs or release-identity checks,
210
+ pin the published version:
209
211
 
210
212
  ```bash
211
- npx @cassiomc1/forgeloop@0.1.13 --version
213
+ npx @cassiomc1/forgeloop@0.1.14 --version
212
214
  npx @cassiomc1/forgeloop init
213
215
  npx @cassiomc1/forgeloop doctor
214
216
  npx @cassiomc1/forgeloop update
@@ -372,12 +374,17 @@ target project. Its main threat boundaries are:
372
374
  | Data exposure | Receipts and checkpoints reject secret-like keys and values; examples use placeholders, and the repository secret scanner runs in CI. |
373
375
  | Unsafe update overwrite | `update` preserves locally modified files and the target's `.forgeloop/kit/PROJECT_PROFILE.md`; adoption and writes remain bounded to the selected target. |
374
376
  | Dependency supply chain | Runtime code uses Node built-ins only; the package does not install agents, providers, plugins, or remote services. |
377
+ | Installation authority provenance | Standalone CLI uses `trustMode: NONE`: environment-selected `FORGELOOP_AUTHORITY_FILE`/`FORGELOOP_AUTHORITY_DIR` sources are untrusted candidates; only an internal `HOST_ATTESTED` context may select a trusted source outside the actor-writable target. Project-local authority claims remain untrusted. |
375
378
  | Stale replay | Work state records contract and repository fingerprints; drift requires revalidation and never reruns destructive or publication actions automatically. |
376
379
  | Unverified publication | Receipts carry explicit publication booleans; local success never implies a push, pull request, merge, release, or deployment. |
377
380
 
378
381
  The full boundary inventory, residual limitations, and executable evidence are
379
382
  in [`THREAT_MODEL.md`](./THREAT_MODEL.md).
380
383
 
384
+ An external path is not equivalent to external authority ownership. Actor-controlled
385
+ environment configuration is not sufficient proof of host/operator authority;
386
+ trusted authority requires a host-attested integration boundary.
387
+
381
388
  The CLI cannot protect a target from a separately privileged or hostile process
382
389
  that changes the filesystem after validation. Consumers must still review
383
390
  permissions, package provenance, and external actions before granting authority.
package/THREAT_MODEL.md CHANGED
@@ -35,6 +35,10 @@ remaining trust boundaries and their executable evidence.
35
35
  | Lifecycle simulation after CLI resolution failure | A runtime manually writes protocol-owned JSON and falsely claims conformance when the CLI is unresolved | Lifecycle-owned protocol state and CLI availability | CLI-owned artifact policy, universal adapter prohibition, validator-backed completion, append-only ledger hash chain | A privileged local process can tamper with local files; ForgeLoop detects consistency, not remote attestation | `tests/discovery-surfaces.test.js`, `tests/lifecycle.test.js`, `LOOP_ENGINEERING.md` |
36
36
  | Unauthorized verification dependency installation | The active actor mutates the environment by downloading or installing a missing checker merely to satisfy verification | Verification capability discovery and installation authority | Missing verification tool policy, non-installing resolution first, explicit installation authority, equivalent-check fallback, NOT_VERIFIED degradation | A privileged external process may still install tools outside ForgeLoop's control | `tests/verification-capability.test.js`, `tests/discovery-surfaces.test.js`, `LOOP_ENGINEERING.md` |
37
37
  | False delegation requirement | A valid single-actor run is incorrectly reported as incomplete because a validator demands artifacts for delegation that never occurred | Validator delegation scope resolution | Canonical delegationIsInScope predicate, conditional delegation validation in validateTaskArtifactSet and validate-protocol | Malformed or externally injected artifacts may require INCONSISTENT classification | `tests/conformance.test.js`, `tests/validate-protocol-cli.test.js` |
38
+ | Self-asserted authority bypass | The active actor supplies `installationAuthorized=true` inside its own verification evidence and thereby grants itself authority | Authority provenance and verification evidence | Boolean claims are ignored; scoped authority references, task/type/tool validation, and validator revalidation in `record-check`/`audit`/`complete` require a trusted source | A host-controlled source is still a semantic trust boundary and is not cryptographic attestation | `tests/verification-capability.test.js`, `tests/authority.test.js`, `tests/stale-receipt-recovery.test.js` |
39
+ | Forged local authority artifact | The active actor creates `.forgeloop/authorities/auth-modlens.json`, claims `source=operator`, and uses the reference to authorize its own installation-capable check | Host/operator authority source versus actor-writable project state | Resolve grants only from an explicit `HOST_ATTESTED` runtime context with an external source outside the target; reject project-local grants with `E_AUTHORITY_UNTRUSTED_SOURCE`; revalidate in `record-check`/`audit`/`complete` | If the host exposes the attested source as writable to the actor, ForgeLoop cannot distinguish genuine grants from actor fabrication without a stronger trust anchor | Local fake rejection, host-attested external acceptance, inside-target rejection, post-record removal and revocation tests |
40
+ | Environment-injected trust root | The active actor creates an authority file outside the project target and sets `FORGELOOP_AUTHORITY_FILE` or `FORGELOOP_AUTHORITY_DIR` when invoking ForgeLoop | Actor-controlled process environment versus host-attested runtime authority | Standalone CLI uses `trustMode: NONE`; environment-selected sources are metadata/candidates only and fail with `E_AUTHORITY_UNTRUSTED_SOURCE`; trusted authority requires an internal `HOST_ATTESTED` context not exposed as a self-assertable CLI option | If the host integration allows the actor to mutate or replace the attested source/context, the host trust boundary is compromised | Environment-injection rejection, standalone audit/complete rejection, host-attested positive path |
41
+ | Stale receipt recovery dead-end | Work state changes after preparing a receipt, leaving a mismatch that cannot be refreshed because the old receipt is rejected during re-preparation | Preparation lifecycle and recovery action resolution | Recoverable stale receipt binding in `prepareCompletion`, executable `PREPARE_COMPLETION` return from `next`, atomic refresh of stateFingerprint and changedPaths | Manual file corruption outside CLI commands requires manual diagnostic recovery | `tests/stale-receipt-recovery.test.js`, `tests/next-executability.test.js` |
38
42
 
39
43
  ## Boundary rules
40
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cassiomc1/forgeloop",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Portable, verifiable engineering protocol for AI coding environments and developer workflows",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "forgeloop://schemas/authority.schema.json",
4
+ "title": "ForgeLoop installation authority grant",
5
+ "type": "object",
6
+ "required": [
7
+ "schemaVersion",
8
+ "protocolVersion",
9
+ "authorityId",
10
+ "taskId",
11
+ "type",
12
+ "status",
13
+ "scope",
14
+ "source"
15
+ ],
16
+ "properties": {
17
+ "schemaVersion": { "const": 1 },
18
+ "protocolVersion": { "const": 1 },
19
+ "authorityId": { "type": "string", "minLength": 1 },
20
+ "taskId": { "type": "string", "minLength": 1 },
21
+ "type": { "enum": ["SOFTWARE_INSTALLATION"] },
22
+ "status": { "enum": ["AUTHORIZED", "REVOKED", "EXPIRED"] },
23
+ "scope": {
24
+ "type": "object",
25
+ "required": ["tool"],
26
+ "properties": {
27
+ "tool": { "type": "string", "minLength": 1 }
28
+ },
29
+ "additionalProperties": false
30
+ },
31
+ "source": { "enum": ["operator", "host", "project-policy"] }
32
+ },
33
+ "additionalProperties": false
34
+ }
@@ -8,6 +8,8 @@ export function formatInspectResult(report) {
8
8
  `Manifest: ${report.manifest.status}`,
9
9
  `Profile: ${report.profile.mode ?? "unknown"}/${report.profile.status ?? "unknown"}`,
10
10
  `Protocol: v${report.protocol.version}`,
11
+ `Authority source: ${report.authority.sourceType ?? "none configured"} / ${report.authority.trusted ? "TRUSTED" : report.authority.trustMode === "NONE" ? "UNATTESTED" : "UNTRUSTED"}`,
12
+ `Authority trust: ${report.authority.trustMode}`,
11
13
  `State: ${report.state.status}`,
12
14
  `Adapters: ${report.adapters.detected.length} detected`,
13
15
  `Findings: ${report.findings.length}`,
@@ -2,8 +2,8 @@ import { prepareCompletion as prepareCompletionArtifacts } from "../core/complet
2
2
 
3
3
  export { prepareCompletionArtifacts as prepareCompletion };
4
4
 
5
- export async function runPrepareCompletion({ target, packageRoot }) {
6
- return prepareCompletionArtifacts({ target, packageRoot });
5
+ export async function runPrepareCompletion({ target, packageRoot, authorityContext, runtimeContext }) {
6
+ return prepareCompletionArtifacts({ target, packageRoot, authorityContext, runtimeContext });
7
7
  }
8
8
 
9
9
  export function formatPrepareCompletionResult(result) {
package/src/core/audit.js CHANGED
@@ -34,8 +34,8 @@ async function compareChangedPaths(target, packageRoot) {
34
34
  };
35
35
  }
36
36
 
37
- export async function evaluateAudit({ target, packageRoot, strict = false } = {}) {
38
- const completion = await evaluateCompletion({ target, packageRoot, strict });
37
+ export async function evaluateAudit({ target, packageRoot, strict = false, authorityContext, runtimeContext } = {}) {
38
+ const completion = await evaluateCompletion({ target, packageRoot, strict, authorityContext, runtimeContext });
39
39
  let manifest = null;
40
40
  let manifestError = null;
41
41
  try {
@@ -1,4 +1,5 @@
1
1
  import { PROTOCOL_VERSION } from "./protocol.js";
2
+ import { validateVerificationAuthority } from "./verification-capability.js";
2
3
 
3
4
  export const CHECK_SCHEMA_VERSION = 1;
4
5
  export const CHECK_STATUSES = Object.freeze(["passed", "failed", "blocked", "not-run"]);
@@ -45,7 +46,7 @@ function assertCompoundStatus(value, label) {
45
46
  }
46
47
  }
47
48
 
48
- export function createCheck(input = {}) {
49
+ export function createCheck(input = {}, options = {}) {
49
50
  const check = {
50
51
  schemaVersion: CHECK_SCHEMA_VERSION,
51
52
  protocolVersion: PROTOCOL_VERSION,
@@ -61,10 +62,10 @@ export function createCheck(input = {}) {
61
62
  ...(input.viewport !== undefined ? { viewport: structuredClone(input.viewport) } : {}),
62
63
  ...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
63
64
  };
64
- return assertCheck(check);
65
+ return assertCheck(check, "check", options);
65
66
  }
66
67
 
67
- export function assertCheck(value, label = "check") {
68
+ export function assertCheck(value, label = "check", options = {}) {
68
69
  if (!value || typeof value !== "object" || Array.isArray(value)) {
69
70
  throw checkError("E_CHECK_INVALID", `${label} must be an object`);
70
71
  }
@@ -97,15 +98,21 @@ export function assertCheck(value, label = "check") {
97
98
  if (value.status === "not-run" && value.evidenceKind !== "NOT_VERIFIED") {
98
99
  throw contradiction(`${label} not-run must use NOT_VERIFIED evidence`);
99
100
  }
101
+ if (value.status === "passed") {
102
+ const auth = validateVerificationAuthority(value, options);
103
+ if (!auth.valid) {
104
+ throw checkError(auth.error.code, auth.error.message);
105
+ }
106
+ }
100
107
  assertCompoundStatus(value, label);
101
108
  return value;
102
109
  }
103
110
 
104
- export function assertCheckList(value, label = "checks") {
111
+ export function assertCheckList(value, label = "checks", options = {}) {
105
112
  if (!Array.isArray(value)) throw checkError("E_CHECK_INVALID", `${label} must be an array`);
106
113
  const ids = new Set();
107
114
  value.forEach((item, index) => {
108
- assertCheck(item, `${label}[${index}]`);
115
+ assertCheck(item, `${label}[${index}]`, options);
109
116
  if (ids.has(item.id)) throw checkError("E_CHECK_INVALID", `${label} contains duplicate id ${item.id}`);
110
117
  ids.add(item.id);
111
118
  });
@@ -11,7 +11,7 @@ import { completionEvidenceForGuides } from "./guide-metadata.js";
11
11
  import { createCheck } from "./checks.js";
12
12
  import { createEvidence } from "./evidence.js";
13
13
  import { coverageForRequirements } from "./coverage.js";
14
- import { assertCompletionRelationships } from "./completion-relationships.js";
14
+ import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
15
15
  import { evaluatePreflight } from "./preflight.js";
16
16
  import { currentChangedPaths } from "./repository.js";
17
17
  import { readPersistedRoute } from "./route-artifact.js";
@@ -19,6 +19,7 @@ import { createReceipt, validateReceipt } from "./receipt.js";
19
19
  import { readWorkState, writeWorkState } from "./work-state.js";
20
20
  import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
21
21
  import { normalizeRequirements, classifyRequirement } from "./evidence-readiness.js";
22
+ import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
22
23
 
23
24
  function artifactError(code, message, artifacts = []) {
24
25
  const error = new Error(message);
@@ -69,7 +70,7 @@ export async function requiredEvidenceForTarget({ target, contract, route, packa
69
70
  ])].sort();
70
71
  }
71
72
 
72
- export async function prepareCompletion({ target, packageRoot }) {
73
+ export async function prepareCompletion({ target, packageRoot, authorityContext, runtimeContext }) {
73
74
  const contract = await readContract(target, packageRoot);
74
75
  const route = await readPersistedRoute(target, packageRoot);
75
76
  const state = await readWorkState(target, packageRoot);
@@ -83,7 +84,12 @@ export async function prepareCompletion({ target, packageRoot }) {
83
84
  let existing = null;
84
85
  try {
85
86
  existing = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
86
- await validateReceipt(existing.value, packageRoot);
87
+ await validateReceipt(existing.value, packageRoot, {
88
+ target,
89
+ taskId: contract?.value?.taskId,
90
+ authorityContext,
91
+ runtimeContext,
92
+ });
87
93
  } catch (error) {
88
94
  if (error.code !== "ARTIFACT_MISSING") throw error;
89
95
  }
@@ -97,17 +103,19 @@ export async function prepareCompletion({ target, packageRoot }) {
97
103
  additionalEvidence: preflight.policy?.requiredEvidence ?? [],
98
104
  });
99
105
  const existingValue = existing?.value ?? {};
100
- assertCompletionRelationships({
101
- contract,
102
- route,
103
- state,
104
- receipt: existingValue.taskId ? existingValue : null,
105
- requiredEvidence,
106
- requireRequiredChecks: false,
107
- });
108
- const changedPaths = existing
109
- ? [...(existingValue.changedPaths ?? [])]
110
- : (await currentChangedPaths(target) ?? []);
106
+ if (existingValue.taskId && existingValue.taskId !== contract.value.taskId) {
107
+ throw artifactError("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.receipt]);
108
+ }
109
+ if (existing && existingValue.stateFingerprint === undefined) {
110
+ throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.receipt]);
111
+ }
112
+ assertStateIdentity({ contract, route, state });
113
+ const observedPaths = await currentChangedPaths(target);
114
+ const changedPaths = observedPaths !== null
115
+ ? [...observedPaths]
116
+ : existing
117
+ ? [...(existingValue.changedPaths ?? [])]
118
+ : [];
111
119
  const checks = existing ? [...existingValue.checks] : [...state.checks];
112
120
  const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
113
121
  const receipt = await createReceipt({
@@ -126,7 +134,11 @@ export async function prepareCompletion({ target, packageRoot }) {
126
134
  changedPaths,
127
135
  checks,
128
136
  evidence,
129
- evidenceCoverage: coverageForRequirements(requiredEvidence, checks),
137
+ evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
138
+ target,
139
+ taskId: contract.value.taskId,
140
+ options: { authorityContext, runtimeContext },
141
+ }),
130
142
  review: existingValue.review ?? { status: "not-run", independent: false },
131
143
  limitations: [...(existingValue.limitations ?? [])],
132
144
  publication: existingValue.publication ?? {
@@ -135,7 +147,24 @@ export async function prepareCompletion({ target, packageRoot }) {
135
147
  pullRequest: null,
136
148
  deployed: false,
137
149
  },
138
- }, packageRoot);
150
+ }, packageRoot, {
151
+ target,
152
+ taskId: contract.value.taskId,
153
+ authorityContext,
154
+ runtimeContext,
155
+ });
156
+ assertCompletionRelationships({
157
+ contract,
158
+ route,
159
+ state,
160
+ receipt,
161
+ requiredEvidence,
162
+ requireRequiredChecks: false,
163
+ target,
164
+ taskId: contract.value.taskId,
165
+ authorityContext,
166
+ runtimeContext,
167
+ });
139
168
  const written = await writeJsonArtifact(
140
169
  target,
141
170
  ARTIFACT_PATHS.receipt,
@@ -179,6 +208,8 @@ export async function recordCheck({
179
208
  result,
180
209
  exitCode,
181
210
  details,
211
+ authorityContext,
212
+ runtimeContext,
182
213
  }) {
183
214
  requiredString(id, "check id");
184
215
  requiredString(kind, "check kind");
@@ -199,6 +230,8 @@ export async function recordCheck({
199
230
  throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
200
231
  }
201
232
 
233
+ const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
234
+
202
235
  const state = await readWorkState(target, packageRoot);
203
236
  if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
204
237
  if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
@@ -235,9 +268,20 @@ export async function recordCheck({
235
268
  }
236
269
 
237
270
  const existingReceipt = await readCurrentReceipt(target, packageRoot);
238
- await validateReceipt(existingReceipt.value, packageRoot);
239
- const source = command?.trim() || `check:${id}`;
240
- const recordedResult = result?.trim() || `recorded command: ${command.trim()}`;
271
+ await validateReceipt(existingReceipt.value, packageRoot, {
272
+ target,
273
+ taskId: contract.value.taskId,
274
+ authorityContext,
275
+ runtimeContext,
276
+ });
277
+ const source = commandSpec || `check:${id}`;
278
+ const recordedResult = result?.trim() || `recorded command: ${commandSpec || source}`;
279
+ const classification = commandSpec !== undefined ? classifyCommandResolution(commandSpec) : null;
280
+ const installationAuthorized = Boolean(
281
+ details?.installationAuthorized
282
+ || details?.authority?.softwareInstallation === "AUTHORIZED"
283
+ || details?.execution?.installationAuthorized
284
+ );
241
285
  const check = createCheck({
242
286
  id,
243
287
  kind,
@@ -252,7 +296,20 @@ export async function recordCheck({
252
296
  ...(result === undefined ? {} : { result }),
253
297
  ...(details === undefined ? {} : details),
254
298
  verificationCycle: state.verificationCycle ?? 1,
299
+ ...(classification ? {
300
+ execution: {
301
+ resolutionMode: classification.resolutionMode,
302
+ mayInstall: classification.mayInstall,
303
+ installationAuthorized,
304
+ },
305
+ } : {}),
255
306
  },
307
+ }, {
308
+ target,
309
+ taskId: contract.value.taskId,
310
+ packageRoot,
311
+ authorityContext,
312
+ runtimeContext,
256
313
  });
257
314
  const evidence = createEvidence({
258
315
  kind: evidenceKind,
@@ -265,6 +322,19 @@ export async function recordCheck({
265
322
  },
266
323
  });
267
324
 
325
+ if (status === "passed") {
326
+ const auth = validateVerificationAuthority(check, {
327
+ target,
328
+ taskId: contract.value.taskId,
329
+ packageRoot,
330
+ authorityContext,
331
+ runtimeContext,
332
+ });
333
+ if (!auth.valid) {
334
+ throw artifactError(auth.error.code, auth.error.message, [ARTIFACT_PATHS.receipt]);
335
+ }
336
+ }
337
+
268
338
  const checks = mergeByCheckId(existingReceipt.value.checks ?? [], check);
269
339
  const evidenceList = appendUniqueEvidence(existingReceipt.value.evidence ?? [], evidence);
270
340
  assertCompletionRelationships({
@@ -274,6 +344,10 @@ export async function recordCheck({
274
344
  receipt: existingReceipt.value,
275
345
  requiredEvidence,
276
346
  requireRequiredChecks: false,
347
+ target,
348
+ taskId: contract.value.taskId,
349
+ authorityContext,
350
+ runtimeContext,
277
351
  });
278
352
  const ledger = await validateEventLedger(target, packageRoot);
279
353
  if (!ledger.valid) {
@@ -287,7 +361,11 @@ export async function recordCheck({
287
361
  [ARTIFACT_PATHS.events],
288
362
  );
289
363
  }
290
- const coverage = coverageForRequirements(requiredEvidence, checks);
364
+ const coverage = coverageForRequirements(requiredEvidence, checks, {
365
+ target,
366
+ taskId: contract.value.taskId,
367
+ options: { authorityContext, runtimeContext },
368
+ });
291
369
  const nextState = {
292
370
  ...state,
293
371
  checks,
@@ -302,7 +380,12 @@ export async function recordCheck({
302
380
  evidenceCoverage: coverage,
303
381
  stateFingerprint: canonicalFingerprint(nextState),
304
382
  verificationCycle: state.verificationCycle ?? 1,
305
- }, packageRoot);
383
+ }, packageRoot, {
384
+ target,
385
+ taskId: contract.value.taskId,
386
+ authorityContext,
387
+ runtimeContext,
388
+ });
306
389
 
307
390
  assertCompletionRelationships({
308
391
  contract,
@@ -311,6 +394,10 @@ export async function recordCheck({
311
394
  receipt: nextReceipt,
312
395
  requiredEvidence,
313
396
  requireRequiredChecks: false,
397
+ target,
398
+ taskId: contract.value.taskId,
399
+ authorityContext,
400
+ runtimeContext,
314
401
  });
315
402
 
316
403
  await writeWorkState(target, nextState, { packageRoot });
@@ -351,6 +438,8 @@ export async function recordTerminalResult({
351
438
  source,
352
439
  result,
353
440
  details = {},
441
+ authorityContext,
442
+ runtimeContext,
354
443
  } = {}) {
355
444
  if (!target || !requirement || !type || !status || !source || !result) {
356
445
  throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
@@ -413,7 +502,12 @@ export async function recordTerminalResult({
413
502
  }
414
503
 
415
504
  const existingReceipt = await readCurrentReceipt(target, packageRoot);
416
- await validateReceipt(existingReceipt.value, packageRoot);
505
+ await validateReceipt(existingReceipt.value, packageRoot, {
506
+ target,
507
+ taskId: contract?.value?.taskId,
508
+ authorityContext,
509
+ runtimeContext,
510
+ });
417
511
 
418
512
  if (type === "PUBLICATION") {
419
513
  const rank = {
@@ -540,7 +634,12 @@ export async function recordTerminalResult({
540
634
  const nextReceipt = await createReceipt({
541
635
  ...receiptUpdates,
542
636
  stateFingerprint: canonicalFingerprint(nextState),
543
- }, packageRoot);
637
+ }, packageRoot, {
638
+ target,
639
+ taskId: contract.value.taskId,
640
+ authorityContext,
641
+ runtimeContext,
642
+ });
544
643
 
545
644
  await writeWorkState(target, nextState, { packageRoot });
546
645
  await writeJsonArtifact(
@@ -8,6 +8,10 @@ export const RECOVERABLE_COMPLETION_EVIDENCE_CODES = Object.freeze([
8
8
  "E_VERIFICATION_CHECK_REQUIRED",
9
9
  "E_CHECK_REQUIRED",
10
10
  "E_CHECK_INVALID",
11
+ "E_INSTALLATION_AUTHORITY_REQUIRED",
12
+ "E_AUTHORITY_INVALID",
13
+ "E_AUTHORITY_SCOPE_MISMATCH",
14
+ "E_AUTHORITY_UNTRUSTED_SOURCE",
11
15
  ]);
12
16
 
13
17
  export function isRecoverableCompletionEvidenceCode(code) {