@cassiomc1/forgeloop 0.1.13 → 0.1.15
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/.forgeloop/forgeloop.gitignore +1 -0
- package/LOOP_ENGINEERING.md +61 -5
- package/PROTOCOL_INTEGRATION.md +104 -0
- package/README.md +34 -16
- package/THREAT_MODEL.md +9 -0
- package/package.json +1 -1
- package/schemas/authority.schema.json +34 -0
- package/schemas/check.schema.json +2 -0
- package/schemas/execution.schema.json +63 -0
- package/src/cli.js +64 -12
- package/src/commands/inspect.js +2 -0
- package/src/commands/prepare-completion.js +2 -2
- package/src/commands/run-check.js +83 -0
- package/src/commands/validate-protocol.js +18 -0
- package/src/core/artifacts.js +8 -0
- package/src/core/audit.js +2 -2
- package/src/core/bundles.js +72 -0
- package/src/core/checks.js +28 -5
- package/src/core/completion-artifacts.js +346 -65
- package/src/core/completion-recovery.js +4 -0
- package/src/core/completion-relationships.js +39 -4
- package/src/core/completion.js +48 -8
- package/src/core/coverage.js +15 -2
- package/src/core/evidence-readiness.js +57 -6
- package/src/core/execution.js +185 -0
- 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/schema-validation.js +1 -0
- package/src/core/templates.js +1 -0
- package/src/core/trusted-authority.js +296 -0
- package/src/core/verification-capability.js +1110 -0
package/src/core/phase.js
CHANGED
|
@@ -101,7 +101,7 @@ function reconcileImplementationStep(state, toPhase) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
|
|
104
|
+
async function assertPhasePrerequisites(target, state, toPhase, packageRoot, authorityContext, runtimeContext) {
|
|
105
105
|
if (toPhase === "CONTRACT_READY" || toPhase === "ROUTED" || toPhase === "EXECUTING") {
|
|
106
106
|
try {
|
|
107
107
|
await readContract(target, packageRoot);
|
|
@@ -127,7 +127,7 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
|
|
|
127
127
|
throw phaseError("E_PHASE_EVIDENCE_MISSING", "COMPLETE requires verification evidence");
|
|
128
128
|
}
|
|
129
129
|
if (toPhase === "COMPLETE") {
|
|
130
|
-
const completion = await evaluateCompletion({ target, packageRoot, persist: false });
|
|
130
|
+
const completion = await evaluateCompletion({ target, packageRoot, persist: false, authorityContext, runtimeContext });
|
|
131
131
|
if (completion.status !== "VALID") {
|
|
132
132
|
throw phaseError("E_COMPLETION_REJECTED", "COMPLETE requires a valid completion audit", completion.errors.flatMap((error) => error.artifacts ?? []));
|
|
133
133
|
}
|
|
@@ -136,11 +136,16 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
|
|
|
136
136
|
|
|
137
137
|
export async function advanceWorkState(target, toPhase, options = {}) {
|
|
138
138
|
const normalizedOptions = typeof options === "string" ? { packageRoot: options } : options;
|
|
139
|
-
const {
|
|
139
|
+
const {
|
|
140
|
+
packageRoot,
|
|
141
|
+
now = new Date().toISOString(),
|
|
142
|
+
authorityContext,
|
|
143
|
+
runtimeContext,
|
|
144
|
+
} = normalizedOptions;
|
|
140
145
|
assertWorkPhase(toPhase);
|
|
141
146
|
const state = await readWorkState(target, packageRoot);
|
|
142
147
|
if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [ARTIFACT_PATHS.state]);
|
|
143
|
-
await assertPhasePrerequisites(target, state, toPhase, packageRoot);
|
|
148
|
+
await assertPhasePrerequisites(target, state, toPhase, packageRoot, authorityContext, runtimeContext);
|
|
144
149
|
await assertPersistedStateIdentity(target, state, toPhase, packageRoot);
|
|
145
150
|
if (!isValidTransition(state.phase, toPhase)) {
|
|
146
151
|
throw phaseError("E_PHASE_TRANSITION_INVALID", `Invalid work-state transition: ${state.phase} -> ${toPhase}`);
|
|
@@ -231,7 +236,12 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
231
236
|
packageRoot,
|
|
232
237
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
233
238
|
});
|
|
234
|
-
await validateReceipt(receipt.value, packageRoot
|
|
239
|
+
await validateReceipt(receipt.value, packageRoot, {
|
|
240
|
+
target,
|
|
241
|
+
taskId: contract?.value?.taskId,
|
|
242
|
+
authorityContext,
|
|
243
|
+
runtimeContext,
|
|
244
|
+
});
|
|
235
245
|
assertCompletionRelationships({
|
|
236
246
|
contract,
|
|
237
247
|
route,
|
|
@@ -240,12 +250,21 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
240
250
|
requiredEvidence,
|
|
241
251
|
requireRequiredChecks: false,
|
|
242
252
|
requireReceiptStateFingerprint: false,
|
|
253
|
+
target,
|
|
254
|
+
taskId: contract?.value?.taskId,
|
|
255
|
+
authorityContext,
|
|
256
|
+
runtimeContext,
|
|
243
257
|
});
|
|
244
258
|
nextReceipt = await createReceipt({
|
|
245
259
|
...receipt.value,
|
|
246
260
|
stateFingerprint: canonicalFingerprint(next),
|
|
247
261
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
248
|
-
}, packageRoot
|
|
262
|
+
}, packageRoot, {
|
|
263
|
+
target,
|
|
264
|
+
taskId: contract?.value?.taskId,
|
|
265
|
+
authorityContext,
|
|
266
|
+
runtimeContext,
|
|
267
|
+
});
|
|
249
268
|
assertCompletionRelationships({
|
|
250
269
|
contract,
|
|
251
270
|
route,
|
|
@@ -253,6 +272,10 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
253
272
|
receipt: nextReceipt,
|
|
254
273
|
requiredEvidence,
|
|
255
274
|
requireRequiredChecks: false,
|
|
275
|
+
target,
|
|
276
|
+
taskId: contract?.value?.taskId,
|
|
277
|
+
authorityContext,
|
|
278
|
+
runtimeContext,
|
|
256
279
|
});
|
|
257
280
|
} catch (error) {
|
|
258
281
|
if (error.code !== "ARTIFACT_MISSING") throw error;
|
package/src/core/protocol.js
CHANGED
|
@@ -81,6 +81,11 @@ export const FAILURE_CODES = Object.freeze([
|
|
|
81
81
|
"E_TERMINAL_REQUIREMENT_NOT_TERMINAL",
|
|
82
82
|
"E_TERMINAL_REQUIREMENT_TYPE_MISMATCH",
|
|
83
83
|
"E_TERMINAL_STATUS_REGRESSION",
|
|
84
|
+
"E_INSTALLATION_AUTHORITY_REQUIRED",
|
|
85
|
+
"E_AUTHORITY_INVALID",
|
|
86
|
+
"E_AUTHORITY_SCOPE_MISMATCH",
|
|
87
|
+
"E_AUTHORITY_UNTRUSTED_SOURCE",
|
|
88
|
+
"E_VERIFICATION_TOOL_UNAVAILABLE",
|
|
84
89
|
]);
|
|
85
90
|
|
|
86
91
|
export const WORK_PHASES = Object.freeze([
|
package/src/core/receipt.js
CHANGED
|
@@ -56,7 +56,7 @@ function assertKnownGuides(guides) {
|
|
|
56
56
|
if (new Set(guides).size !== guides.length) throw new Error("Receipt selectedGuides must not contain duplicates");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function assertReceiptSemantics(receipt) {
|
|
59
|
+
export function assertReceiptSemantics(receipt, options = {}) {
|
|
60
60
|
const evidence = receipt.evidence ?? [];
|
|
61
61
|
if (receipt.verificationCycle !== undefined
|
|
62
62
|
&& (!Number.isInteger(receipt.verificationCycle) || receipt.verificationCycle < 1)) {
|
|
@@ -66,7 +66,7 @@ export function assertReceiptSemantics(receipt) {
|
|
|
66
66
|
|
|
67
67
|
for (const [index, check] of receipt.checks.entries()) {
|
|
68
68
|
if (check?.schemaVersion !== undefined || check?.id !== undefined || check?.evidenceKind !== undefined) {
|
|
69
|
-
assertCheck(check, `receipt.checks[${index}]
|
|
69
|
+
assertCheck(check, `receipt.checks[${index}]`, options);
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
72
72
|
if (check?.status === "passed") {
|
|
@@ -132,15 +132,15 @@ export function assertReceiptSemantics(receipt) {
|
|
|
132
132
|
return receipt;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
export async function validateReceipt(receipt, packageRoot) {
|
|
135
|
+
export async function validateReceipt(receipt, packageRoot, options = {}) {
|
|
136
136
|
assertSecretFree(receipt);
|
|
137
137
|
const schema = await readSchema("execution-receipt", packageRoot);
|
|
138
138
|
assertSchema(receipt, schema, "execution receipt");
|
|
139
139
|
assertKnownGuides(receipt.selectedGuides);
|
|
140
|
-
return assertReceiptSemantics(receipt);
|
|
140
|
+
return assertReceiptSemantics(receipt, options);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
export async function createReceipt(input, packageRoot) {
|
|
143
|
+
export async function createReceipt(input, packageRoot, options = {}) {
|
|
144
144
|
assertSecretFree(input);
|
|
145
145
|
const receipt = {
|
|
146
146
|
schemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
@@ -169,5 +169,5 @@ export async function createReceipt(input, packageRoot) {
|
|
|
169
169
|
deployed: false,
|
|
170
170
|
},
|
|
171
171
|
};
|
|
172
|
-
return validateReceipt(receipt, packageRoot);
|
|
172
|
+
return validateReceipt(receipt, packageRoot, options);
|
|
173
173
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const AUTHORITY_TRUST_MODES = Object.freeze(["NONE", "HOST_ATTESTED"]);
|
|
2
|
+
|
|
3
|
+
const AUTHORITY_CONTEXT_FIELDS = Object.freeze([
|
|
4
|
+
"trustedAuthorityFile",
|
|
5
|
+
"trustedAuthorityDir",
|
|
6
|
+
"authorities",
|
|
7
|
+
"authority",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function configuredValue(value) {
|
|
11
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function providerValue(provider) {
|
|
15
|
+
if (typeof provider === "function") return provider();
|
|
16
|
+
return provider && typeof provider === "object" && !Array.isArray(provider) ? provider : {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function contextInput(input = {}) {
|
|
20
|
+
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
21
|
+
const provider = providerValue(source.trustedAuthorityProvider);
|
|
22
|
+
const explicit = source.authorityContext && typeof source.authorityContext === "object"
|
|
23
|
+
? source.authorityContext
|
|
24
|
+
: {};
|
|
25
|
+
const { authorityContext: _authorityContext, trustedAuthorityProvider: _trustedAuthorityProvider, ...direct } = source;
|
|
26
|
+
return { ...direct, ...provider, ...explicit };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasAuthorityContext(options = {}) {
|
|
30
|
+
if (!options || typeof options !== "object") return false;
|
|
31
|
+
return Boolean(
|
|
32
|
+
options.authorityContext
|
|
33
|
+
|| options.runtimeContext?.authorityContext
|
|
34
|
+
|| (options.runtimeContext && typeof options.runtimeContext === "object" && options.runtimeContext.trustMode),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createAuthorityContext(input = {}) {
|
|
39
|
+
const values = contextInput(input);
|
|
40
|
+
const trustMode = values.trustMode ?? values.authorityTrustMode ?? "NONE";
|
|
41
|
+
if (!AUTHORITY_TRUST_MODES.includes(trustMode)) {
|
|
42
|
+
const error = new Error(`Unsupported authority trust mode: ${trustMode}`);
|
|
43
|
+
error.code = "E_AUTHORITY_INVALID";
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const context = { trustMode };
|
|
48
|
+
for (const field of AUTHORITY_CONTEXT_FIELDS) {
|
|
49
|
+
if (field === "trustedAuthorityFile" || field === "trustedAuthorityDir") {
|
|
50
|
+
const value = configuredValue(values[field]);
|
|
51
|
+
if (value !== undefined) context[field] = value;
|
|
52
|
+
} else if (values[field] !== undefined) {
|
|
53
|
+
context[field] = values[field];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return Object.freeze(context);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveAuthorityContext(options = {}) {
|
|
60
|
+
const source = options && typeof options === "object" && !Array.isArray(options) ? options : {};
|
|
61
|
+
if (source.authorityContext !== undefined) {
|
|
62
|
+
return createAuthorityContext(source.authorityContext);
|
|
63
|
+
}
|
|
64
|
+
if (source.runtimeContext?.authorityContext !== undefined) {
|
|
65
|
+
return createAuthorityContext(source.runtimeContext.authorityContext);
|
|
66
|
+
}
|
|
67
|
+
if (source.runtimeContext && typeof source.runtimeContext === "object" && source.runtimeContext.trustMode) {
|
|
68
|
+
return createAuthorityContext(source.runtimeContext);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const hasDirectAuthority = AUTHORITY_CONTEXT_FIELDS.some((field) => source[field] !== undefined);
|
|
72
|
+
// Direct resolver options are legacy source selectors, not a host attestation
|
|
73
|
+
// channel. They remain NONE unless wrapped in an explicit runtime context.
|
|
74
|
+
return hasDirectAuthority ? createAuthorityContext({ ...source, trustMode: "NONE" }) : createAuthorityContext();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createForgeLoopContext(options = {}) {
|
|
78
|
+
const authorityContext = createAuthorityContext(options);
|
|
79
|
+
return Object.freeze({ authorityContext });
|
|
80
|
+
}
|
package/src/core/templates.js
CHANGED
|
@@ -59,6 +59,7 @@ export const TEMPLATE_PATHS = [
|
|
|
59
59
|
"schemas/config.schema.json",
|
|
60
60
|
"schemas/preflight.schema.json",
|
|
61
61
|
"schemas/check.schema.json",
|
|
62
|
+
"schemas/execution.schema.json",
|
|
62
63
|
"schemas/evidence-coverage.schema.json",
|
|
63
64
|
"schemas/event.schema.json",
|
|
64
65
|
"schemas/activation.schema.json",
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { accessSync, constants as fsConstants, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
|
|
5
|
+
import {
|
|
6
|
+
hasAuthorityContext,
|
|
7
|
+
resolveAuthorityContext,
|
|
8
|
+
} from "./runtime-context.js";
|
|
9
|
+
|
|
10
|
+
export const E_AUTHORITY_UNTRUSTED_SOURCE = "E_AUTHORITY_UNTRUSTED_SOURCE";
|
|
11
|
+
const E_AUTHORITY_INVALID = "E_AUTHORITY_INVALID";
|
|
12
|
+
const AUTHORITY_FILE_ENV = "FORGELOOP_AUTHORITY_FILE";
|
|
13
|
+
const AUTHORITY_DIR_ENV = "FORGELOOP_AUTHORITY_DIR";
|
|
14
|
+
|
|
15
|
+
function configuredValue(value) {
|
|
16
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function authoritySourceOptions(options = {}) {
|
|
20
|
+
const context = resolveAuthorityContext(options);
|
|
21
|
+
const contextProvided = hasAuthorityContext(options);
|
|
22
|
+
const directFile = configuredValue(options.trustedAuthorityFile);
|
|
23
|
+
const directDir = configuredValue(options.trustedAuthorityDir);
|
|
24
|
+
const contextFile = configuredValue(context.trustedAuthorityFile);
|
|
25
|
+
const contextDir = configuredValue(context.trustedAuthorityDir);
|
|
26
|
+
const file = contextFile ?? (contextProvided ? null : directFile);
|
|
27
|
+
const dir = contextDir ?? (contextProvided ? null : directDir);
|
|
28
|
+
const authorities = context.authorities ?? (contextProvided ? undefined : options.authorities);
|
|
29
|
+
const authority = context.authority ?? (contextProvided ? undefined : options.authority);
|
|
30
|
+
|
|
31
|
+
if (contextProvided
|
|
32
|
+
&& context.trustMode !== "HOST_ATTESTED"
|
|
33
|
+
&& !file
|
|
34
|
+
&& !dir
|
|
35
|
+
&& authorities === undefined
|
|
36
|
+
&& authority === undefined) {
|
|
37
|
+
const envFile = configuredValue(process.env[AUTHORITY_FILE_ENV]);
|
|
38
|
+
const envDir = configuredValue(process.env[AUTHORITY_DIR_ENV]);
|
|
39
|
+
return {
|
|
40
|
+
context,
|
|
41
|
+
file: envFile,
|
|
42
|
+
dir: envDir,
|
|
43
|
+
authorities: undefined,
|
|
44
|
+
authority: undefined,
|
|
45
|
+
sourceConfigured: Boolean(envFile || envDir),
|
|
46
|
+
sourceType: envFile ? "external-file" : envDir ? "external-dir" : null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (context.trustMode === "HOST_ATTESTED" || contextProvided || file || dir || authorities !== undefined || authority !== undefined) {
|
|
51
|
+
return {
|
|
52
|
+
context,
|
|
53
|
+
file,
|
|
54
|
+
dir,
|
|
55
|
+
authorities,
|
|
56
|
+
authority,
|
|
57
|
+
sourceConfigured: Boolean(file || dir || authorities !== undefined || authority !== undefined),
|
|
58
|
+
sourceType: file ? "external-file" : dir ? "external-dir" : authorities !== undefined || authority !== undefined ? "in-memory" : null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const envFile = configuredValue(process.env[AUTHORITY_FILE_ENV]);
|
|
63
|
+
const envDir = configuredValue(process.env[AUTHORITY_DIR_ENV]);
|
|
64
|
+
return {
|
|
65
|
+
context,
|
|
66
|
+
file: envFile,
|
|
67
|
+
dir: envDir,
|
|
68
|
+
authorities: undefined,
|
|
69
|
+
authority: undefined,
|
|
70
|
+
sourceConfigured: Boolean(envFile || envDir),
|
|
71
|
+
sourceType: envFile ? "external-file" : envDir ? "external-dir" : null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function trustedAuthorityConfiguration(options = {}) {
|
|
76
|
+
const { context, sourceConfigured, sourceType, file, dir } = authoritySourceOptions(options);
|
|
77
|
+
const sourceInsideTarget = options.target && (file || dir)
|
|
78
|
+
? isInsideTarget(options.target, file ?? dir)
|
|
79
|
+
: false;
|
|
80
|
+
return {
|
|
81
|
+
sourceConfigured,
|
|
82
|
+
sourceType,
|
|
83
|
+
trustMode: context.trustMode,
|
|
84
|
+
trusted: context.trustMode === "HOST_ATTESTED" && sourceConfigured && !sourceInsideTarget,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function invalid(message, details = {}) {
|
|
89
|
+
return {
|
|
90
|
+
trusted: false,
|
|
91
|
+
error: { code: E_AUTHORITY_INVALID, message },
|
|
92
|
+
...details,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function untrusted(message) {
|
|
97
|
+
return {
|
|
98
|
+
trusted: false,
|
|
99
|
+
error: { code: E_AUTHORITY_UNTRUSTED_SOURCE, message },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function authorityFileName(authorityRef) {
|
|
104
|
+
if (typeof authorityRef !== "string" || authorityRef.trim() === "") return null;
|
|
105
|
+
const ref = authorityRef.trim();
|
|
106
|
+
if (ref === "." || ref === ".." || ref.includes("/") || ref.includes("\\")) return null;
|
|
107
|
+
return ref.endsWith(".json") ? ref : `${ref}.json`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolvedPath(candidate) {
|
|
111
|
+
const absolute = path.resolve(candidate);
|
|
112
|
+
const missingSegments = [];
|
|
113
|
+
let current = absolute;
|
|
114
|
+
while (true) {
|
|
115
|
+
try {
|
|
116
|
+
const existing = realpathSync(current);
|
|
117
|
+
return path.resolve(existing, ...missingSegments);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.code !== "ENOENT") throw error;
|
|
120
|
+
const parent = path.dirname(current);
|
|
121
|
+
if (parent === current) return absolute;
|
|
122
|
+
missingSegments.unshift(path.basename(current));
|
|
123
|
+
current = parent;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isWithin(parent, candidate) {
|
|
129
|
+
const relative = path.relative(parent, candidate);
|
|
130
|
+
return relative === ""
|
|
131
|
+
|| (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isInsideTarget(target, candidate) {
|
|
135
|
+
if (!target) return false;
|
|
136
|
+
// Check both lexical paths and their real paths. The lexical check rejects
|
|
137
|
+
// actor-created symlinks inside the target that point at an external file.
|
|
138
|
+
return isWithin(path.resolve(target), path.resolve(candidate))
|
|
139
|
+
|| isWithin(resolvedPath(target), resolvedPath(candidate));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readExternalJson(candidate, target, kind) {
|
|
143
|
+
if (isInsideTarget(target, candidate)) {
|
|
144
|
+
return untrusted(`Configured trusted authority ${kind} must be outside the actor-writable target`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
accessSync(candidate, fsConstants.R_OK);
|
|
149
|
+
const info = lstatSync(candidate);
|
|
150
|
+
if (kind === "file" && !info.isFile()) {
|
|
151
|
+
return invalid("Configured trusted authority file is not a regular file");
|
|
152
|
+
}
|
|
153
|
+
if (kind === "directory" && !info.isDirectory()) {
|
|
154
|
+
return invalid("Configured trusted authority directory is not a directory");
|
|
155
|
+
}
|
|
156
|
+
const bytes = readFileSync(candidate);
|
|
157
|
+
assertJsonBytes(bytes, `trusted authority ${kind}`);
|
|
158
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
159
|
+
assertJsonLimits(value, `trusted authority ${kind}`);
|
|
160
|
+
return { value };
|
|
161
|
+
} catch {
|
|
162
|
+
return invalid(`Configured trusted authority ${kind} could not be read or parsed`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function grantsFromValue(value) {
|
|
167
|
+
if (Array.isArray(value)) return value;
|
|
168
|
+
if (value && typeof value === "object" && Array.isArray(value.authorities)) {
|
|
169
|
+
if (value.schemaVersion !== 1 || value.protocolVersion !== 1) return null;
|
|
170
|
+
return value.authorities;
|
|
171
|
+
}
|
|
172
|
+
return value && typeof value === "object" ? [value] : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function findAuthority(value, authorityRef) {
|
|
176
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
177
|
+
const keyedAuthority = value[authorityRef];
|
|
178
|
+
if (keyedAuthority && typeof keyedAuthority === "object") return keyedAuthority;
|
|
179
|
+
}
|
|
180
|
+
const envelope = value && typeof value === "object" && !Array.isArray(value)
|
|
181
|
+
&& Array.isArray(value.authorities)
|
|
182
|
+
? value
|
|
183
|
+
: null;
|
|
184
|
+
const grants = grantsFromValue(value);
|
|
185
|
+
if (!grants) return null;
|
|
186
|
+
const authority = grants.find((item) => item?.authorityId === authorityRef || item?.id === authorityRef) ?? null;
|
|
187
|
+
if (!authority || !envelope) return authority;
|
|
188
|
+
return {
|
|
189
|
+
...authority,
|
|
190
|
+
schemaVersion: envelope.schemaVersion,
|
|
191
|
+
protocolVersion: envelope.protocolVersion,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function findInMemoryAuthority(authorities, authority, authorityRef) {
|
|
196
|
+
if (authority !== undefined) {
|
|
197
|
+
return authority?.authorityId === authorityRef || authority?.id === authorityRef ? authority : null;
|
|
198
|
+
}
|
|
199
|
+
if (authorities !== undefined) {
|
|
200
|
+
return findAuthority(authorities, authorityRef);
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveFromExternalFile(authorityRef, target, file) {
|
|
206
|
+
const read = readExternalJson(file, target, "file");
|
|
207
|
+
if (read.error) return read;
|
|
208
|
+
const authority = findAuthority(read.value, authorityRef);
|
|
209
|
+
return authority
|
|
210
|
+
? { trusted: true, authority, sourceType: "external-file" }
|
|
211
|
+
: invalid(`Referenced installation authority '${authorityRef}' was not found in the trusted authority file`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveFromExternalDirectory(authorityRef, target, dir) {
|
|
215
|
+
const fileName = authorityFileName(authorityRef);
|
|
216
|
+
if (!fileName) return invalid("Installation authority reference must be a simple authority ID");
|
|
217
|
+
if (isInsideTarget(target, dir)) {
|
|
218
|
+
return untrusted("Configured trusted authority directory must be outside the actor-writable target");
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
accessSync(dir, fsConstants.R_OK);
|
|
222
|
+
if (!lstatSync(dir).isDirectory()) {
|
|
223
|
+
return invalid("Configured trusted authority directory is not a directory");
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
return invalid("Configured trusted authority directory could not be read");
|
|
227
|
+
}
|
|
228
|
+
const authorityPath = path.join(dir, fileName);
|
|
229
|
+
const read = readExternalJson(authorityPath, target, "file");
|
|
230
|
+
if (read.error) return read;
|
|
231
|
+
const authority = findAuthority(read.value, authorityRef);
|
|
232
|
+
return authority
|
|
233
|
+
? { trusted: true, authority, sourceType: "external-dir" }
|
|
234
|
+
: invalid(`Referenced installation authority '${authorityRef}' does not match the trusted authority file`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function projectLocalAuthorityExists(authorityRef, target) {
|
|
238
|
+
const fileName = authorityFileName(authorityRef);
|
|
239
|
+
if (!fileName || !target) return false;
|
|
240
|
+
const localPath = path.join(target, ".forgeloop", "authorities", fileName);
|
|
241
|
+
try {
|
|
242
|
+
accessSync(localPath, fsConstants.F_OK);
|
|
243
|
+
return true;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error.code === "ENOENT") return false;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function resolveTrustedAuthority({
|
|
251
|
+
authorityRef,
|
|
252
|
+
target,
|
|
253
|
+
trustedAuthorityFile,
|
|
254
|
+
trustedAuthorityDir,
|
|
255
|
+
authorities,
|
|
256
|
+
authority,
|
|
257
|
+
authorityContext,
|
|
258
|
+
runtimeContext,
|
|
259
|
+
} = {}) {
|
|
260
|
+
const fileName = authorityFileName(authorityRef);
|
|
261
|
+
if (!fileName) return invalid("Installation authority reference must be a simple authority ID");
|
|
262
|
+
|
|
263
|
+
const sources = authoritySourceOptions({
|
|
264
|
+
trustedAuthorityFile,
|
|
265
|
+
trustedAuthorityDir,
|
|
266
|
+
authorities,
|
|
267
|
+
authority,
|
|
268
|
+
authorityContext,
|
|
269
|
+
runtimeContext,
|
|
270
|
+
});
|
|
271
|
+
if (sources.sourceConfigured && sources.context.trustMode !== "HOST_ATTESTED") {
|
|
272
|
+
return untrusted("Standalone configuration selects an authority source but does not attest host authority");
|
|
273
|
+
}
|
|
274
|
+
if ((sources.file || sources.dir) && !target) {
|
|
275
|
+
return untrusted("A target path is required to validate trusted authority provenance");
|
|
276
|
+
}
|
|
277
|
+
if (sources.file) return resolveFromExternalFile(authorityRef, target, sources.file);
|
|
278
|
+
if (sources.dir) return resolveFromExternalDirectory(authorityRef, target, sources.dir);
|
|
279
|
+
|
|
280
|
+
// In-memory authorities are a host-injected policy interface. They never
|
|
281
|
+
// originate from project-local files and are intentionally resolved only
|
|
282
|
+
// after the explicit external sources above.
|
|
283
|
+
const inMemory = findInMemoryAuthority(sources.authorities, sources.authority, authorityRef);
|
|
284
|
+
if (inMemory !== undefined) {
|
|
285
|
+
return inMemory
|
|
286
|
+
? { trusted: true, authority: inMemory, sourceType: "in-memory" }
|
|
287
|
+
: invalid(`Referenced installation authority '${authorityRef}' could not be resolved`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (projectLocalAuthorityExists(authorityRef, target)) {
|
|
291
|
+
return untrusted("Project-local authority artifacts are references only and are not trusted authority sources");
|
|
292
|
+
}
|
|
293
|
+
return invalid(`Referenced installation authority '${authorityRef}' could not be resolved`, {
|
|
294
|
+
sourceConfigured: sources.sourceConfigured,
|
|
295
|
+
});
|
|
296
|
+
}
|