@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.
- package/LOOP_ENGINEERING.md +33 -0
- package/PROTOCOL_INTEGRATION.md +71 -0
- package/README.md +16 -9
- package/THREAT_MODEL.md +4 -0
- package/package.json +1 -1
- package/schemas/authority.schema.json +34 -0
- package/src/commands/inspect.js +2 -0
- package/src/commands/prepare-completion.js +2 -2
- package/src/core/audit.js +2 -2
- package/src/core/checks.js +12 -5
- package/src/core/completion-artifacts.js +122 -23
- package/src/core/completion-recovery.js +4 -0
- package/src/core/completion-relationships.js +39 -4
- package/src/core/completion.js +29 -7
- package/src/core/coverage.js +15 -2
- package/src/core/evidence-readiness.js +32 -6
- package/src/core/inspect.js +3 -1
- package/src/core/next-action.js +87 -9
- package/src/core/phase.js +29 -6
- package/src/core/protocol.js +5 -0
- package/src/core/receipt.js +6 -6
- package/src/core/runtime-context.js +80 -0
- package/src/core/trusted-authority.js +296 -0
- package/src/core/verification-capability.js +499 -0
package/LOOP_ENGINEERING.md
CHANGED
|
@@ -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.
|
package/PROTOCOL_INTEGRATION.md
CHANGED
|
@@ -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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
|
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
|
@@ -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
|
+
}
|
package/src/commands/inspect.js
CHANGED
|
@@ -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 {
|
package/src/core/checks.js
CHANGED
|
@@ -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
|
-
|
|
101
|
-
contract,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
receipt
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const changedPaths =
|
|
109
|
-
? [...
|
|
110
|
-
:
|
|
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
|
-
|
|
240
|
-
|
|
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) {
|