@cassiomc1/forgeloop 1.8.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/project-loop.mdc +6 -3
- package/.github/copilot-instructions.md +5 -0
- package/AGENTS.md +6 -0
- package/AGENT_COMPATIBILITY.md +15 -0
- package/CLAUDE.md +6 -0
- package/DELEGATION_PROTOCOL.md +6 -0
- package/DOCS_INDEX.md +9 -2
- package/ENG/accessibility-eng.md +12 -2
- package/ENG/design-code-eng.md +22 -1
- package/LOOP_ENGINEERING.md +41 -0
- package/LOOP_SYSTEM_DESIGN.md +33 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +57 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +51 -0
- package/TERMINOLOGY.md +12 -0
- package/THREAT_MODEL.md +48 -0
- package/completions/_forgeloop +5 -1
- package/completions/forgeloop.bash +9 -1
- package/completions/forgeloop.fish +27 -1
- package/docs/ADVISORY_CONTEXT.md +174 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +33 -2
- package/docs/ARTIFACT_REFERENCE.md +142 -0
- package/docs/CLI_REFERENCE.md +124 -1
- package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
- package/docs/DOCUMENTATION_GUIDE.md +7 -0
- package/docs/GETTING_STARTED.md +22 -0
- package/docs/KNOWLEDGE_SOURCES.md +171 -0
- package/docs/MCP.md +17 -1
- package/docs/RECIPES.md +111 -0
- package/docs/RELEASE_CHECKLIST.md +14 -0
- package/docs/STRUCTURAL_QUALITY.md +350 -0
- package/docs/TROUBLESHOOTING.md +161 -2
- package/docs/UNIVERSAL_INTEGRATION.md +60 -0
- package/package.json +4 -1
- package/schemas/config.schema.json +46 -0
- package/schemas/handoff-envelope.schema.json +1 -0
- package/schemas/preflight.schema.json +2 -1
- package/schemas/structural-quality.schema.json +175 -0
- package/scripts/check-changelog-freshness.mjs +27 -3
- package/scripts/generate-agent-protocol-summary.mjs +18 -0
- package/src/cli.js +24 -0
- package/src/commands/handoff-accept.js +36 -0
- package/src/commands/handoff-list.js +28 -2
- package/src/commands/handoff-show.js +27 -2
- package/src/commands/quality-baseline.js +28 -0
- package/src/commands/quality-status.js +34 -0
- package/src/commands/quality-verify.js +30 -0
- package/src/commands/reconcile-continuity.js +4 -0
- package/src/core/advisory-context/constants.js +74 -0
- package/src/core/advisory-context/provider.js +287 -0
- package/src/core/advisory-context/service.js +140 -0
- package/src/core/artifact-registry.js +12 -0
- package/src/core/audit.js +38 -0
- package/src/core/bundles.js +134 -1
- package/src/core/cli-command-definitions.js +62 -0
- package/src/core/command-executors.js +28 -0
- package/src/core/command-input.js +23 -1
- package/src/core/completion-artifacts.js +2 -0
- package/src/core/completion.js +42 -0
- package/src/core/config.js +3 -0
- package/src/core/continuity-lint.js +89 -0
- package/src/core/continuity-reconciliation.js +16 -0
- package/src/core/continuity.js +10 -11
- package/src/core/error-codes.js +186 -0
- package/src/core/events.js +32 -0
- package/src/core/execution-profile-context.js +15 -1
- package/src/core/filesystem.js +34 -3
- package/src/core/handoff-acceptance.js +277 -0
- package/src/core/handoff.js +41 -8
- package/src/core/inspect.js +64 -0
- package/src/core/integration-invocation-policy.js +34 -2
- package/src/core/integration-resources.js +38 -1
- package/src/core/next-action-model.js +11 -1
- package/src/core/next-action-phases.js +84 -5
- package/src/core/phase.js +9 -1
- package/src/core/portable-context.js +103 -0
- package/src/core/preflight.js +33 -0
- package/src/core/protocol-info.js +33 -2
- package/src/core/runtime-context.js +58 -0
- package/src/core/schema-validation.js +1 -0
- package/src/core/structural-quality/artifacts.js +329 -0
- package/src/core/structural-quality/constants.js +67 -0
- package/src/core/structural-quality/policy.js +227 -0
- package/src/core/structural-quality/provider.js +287 -0
- package/src/core/structural-quality/sentrux-mcp.js +477 -0
- package/src/core/structural-quality/service.js +1138 -0
- package/src/core/structural-quality/source-fingerprint.js +112 -0
- package/src/core/structural-quality/status.js +3 -0
- package/src/core/task-paths.js +24 -0
- package/src/core/templates.js +1 -0
- package/src/integration.d.ts +141 -0
- package/src/integration.js +36 -0
package/src/core/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts
|
|
|
3
3
|
import { E_ATTESTATION_CONFIGURATION_INVALID } from "./error-codes.js";
|
|
4
4
|
import { normalizeVerificationConfiguration } from "./verification-scope-capability.js";
|
|
5
5
|
import { EXECUTION_PROFILE_REQUESTS } from "./execution-profile.js";
|
|
6
|
+
import { normalizeStructuralQualityConfig } from "./structural-quality/policy.js";
|
|
6
7
|
|
|
7
8
|
export const CONFIG_SCHEMA_VERSION = 1;
|
|
8
9
|
export const COMPLIANCE_MODES = Object.freeze(["advisory", "standard", "strict"]);
|
|
@@ -80,6 +81,7 @@ export function createConfig(input = {}) {
|
|
|
80
81
|
}
|
|
81
82
|
executionProfile = input.executionProfile;
|
|
82
83
|
}
|
|
84
|
+
const structuralQuality = normalizeStructuralQualityConfig(input.structuralQuality);
|
|
83
85
|
return {
|
|
84
86
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
85
87
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -90,6 +92,7 @@ export function createConfig(input = {}) {
|
|
|
90
92
|
...(verification ? { verification } : {}),
|
|
91
93
|
...(attestation ? { attestation } : {}),
|
|
92
94
|
...(executionProfile ? { executionProfile } : {}),
|
|
95
|
+
...(structuralQuality !== undefined ? { structuralQuality } : {}),
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
2
|
+
|
|
3
|
+
function finding(code, severity, field, itemId = null) {
|
|
4
|
+
return { code, severity, field, itemId };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function hasOperationalHints(continuity) {
|
|
8
|
+
return Boolean(
|
|
9
|
+
continuity.currentFocus
|
|
10
|
+
|| continuity.remainingWork?.length
|
|
11
|
+
|| continuity.knownIssues?.length
|
|
12
|
+
|| continuity.changedAreas?.length
|
|
13
|
+
|| continuity.inspectFirst?.length
|
|
14
|
+
|| (typeof continuity.resumeNote === "string" && continuity.resumeNote.trim() !== ""),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function lintContinuity({ target, continuity, state } = {}) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
if (!continuity || typeof continuity !== "object" || Array.isArray(continuity)) {
|
|
21
|
+
return { status: "PASS", findings };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const completedSteps = new Set(
|
|
25
|
+
Array.isArray(state?.completedSteps) ? state.completedSteps : [],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
for (const [index, item] of (continuity.remainingWork ?? []).entries()) {
|
|
29
|
+
if (!item?.id) continue;
|
|
30
|
+
if (completedSteps.has(item.id)) {
|
|
31
|
+
findings.push(finding(
|
|
32
|
+
"CONTINUITY_REMAINING_ALREADY_COMPLETED",
|
|
33
|
+
"WARN",
|
|
34
|
+
`remainingWork[${index}]`,
|
|
35
|
+
item.id,
|
|
36
|
+
));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (continuity.currentFocus?.id && completedSteps.has(continuity.currentFocus.id)) {
|
|
41
|
+
findings.push(finding(
|
|
42
|
+
"CONTINUITY_FOCUS_ALREADY_COMPLETED",
|
|
43
|
+
"WARN",
|
|
44
|
+
"currentFocus",
|
|
45
|
+
continuity.currentFocus.id,
|
|
46
|
+
));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const knownIssueIds = new Set(
|
|
50
|
+
(continuity.knownIssues ?? []).map((item) => item?.id).filter(Boolean),
|
|
51
|
+
);
|
|
52
|
+
for (const [index, item] of (continuity.remainingWork ?? []).entries()) {
|
|
53
|
+
if (item?.id && knownIssueIds.has(item.id)) {
|
|
54
|
+
findings.push(finding(
|
|
55
|
+
"CONTINUITY_ITEM_ROLE_CONFLICT",
|
|
56
|
+
"WARN",
|
|
57
|
+
`remainingWork[${index}]`,
|
|
58
|
+
item.id,
|
|
59
|
+
));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof target === "string" && target.trim() !== "") {
|
|
64
|
+
for (const [index, inspectPath] of (continuity.inspectFirst ?? []).entries()) {
|
|
65
|
+
try {
|
|
66
|
+
await assertSafePath(target, inspectPath);
|
|
67
|
+
if (!(await fileExists(ensureWithin(target, inspectPath)))) {
|
|
68
|
+
findings.push(finding(
|
|
69
|
+
"CONTINUITY_INSPECT_PATH_MISSING",
|
|
70
|
+
"WARN",
|
|
71
|
+
`inspectFirst[${index}]`,
|
|
72
|
+
));
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Invalid or unsafe paths are rejected by continuity schema validation;
|
|
76
|
+
// lint never resolves an unchecked path or turns it into authority.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!hasOperationalHints(continuity)) {
|
|
82
|
+
findings.push(finding("CONTINUITY_EMPTY_HINT_SET", "INFO", "continuity"));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
status: findings.some((item) => item.severity === "WARN") ? "WARN" : "PASS",
|
|
87
|
+
findings,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canonicalFingerprint } from "./artifacts.js";
|
|
2
2
|
import { assertContinuitySemantics, readContinuity } from "./continuity.js";
|
|
3
3
|
import { WORK_TRANSITIONS } from "./protocol.js";
|
|
4
|
+
import { lintContinuity } from "./continuity-lint.js";
|
|
4
5
|
|
|
5
6
|
const RECONCILIATION_CODE = "E_CONTINUITY_RECONCILIATION_REQUIRED";
|
|
6
7
|
|
|
@@ -188,6 +189,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
|
|
|
188
189
|
path: ".forgeloop/continuity.json",
|
|
189
190
|
present: false,
|
|
190
191
|
latestHandoff,
|
|
192
|
+
lint: { status: "PASS", findings: [] },
|
|
191
193
|
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
192
194
|
};
|
|
193
195
|
}
|
|
@@ -200,6 +202,15 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
|
|
|
200
202
|
present: true,
|
|
201
203
|
error: error.message,
|
|
202
204
|
latestHandoff,
|
|
205
|
+
lint: {
|
|
206
|
+
status: "WARN",
|
|
207
|
+
findings: [{
|
|
208
|
+
code: error.code ?? "CONTINUITY_INVALID",
|
|
209
|
+
severity: "WARN",
|
|
210
|
+
field: "continuity",
|
|
211
|
+
itemId: null,
|
|
212
|
+
}],
|
|
213
|
+
},
|
|
203
214
|
};
|
|
204
215
|
}
|
|
205
216
|
|
|
@@ -229,6 +240,11 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
|
|
|
229
240
|
fingerprint: continuityArtifact.fingerprint,
|
|
230
241
|
continuity: continuityArtifact.value,
|
|
231
242
|
latestHandoff,
|
|
243
|
+
lint: await lintContinuity({
|
|
244
|
+
target,
|
|
245
|
+
continuity: continuityArtifact.value,
|
|
246
|
+
state,
|
|
247
|
+
}),
|
|
232
248
|
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
233
249
|
};
|
|
234
250
|
}
|
package/src/core/continuity.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./artifacts.js";
|
|
10
10
|
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
11
11
|
import { PROTOCOL_VERSION, WORK_PHASES } from "./protocol.js";
|
|
12
|
-
import {
|
|
12
|
+
import { normalizePortableText, assertPortableContextSafe } from "./portable-context.js";
|
|
13
13
|
import { getPackageRoot } from "./templates.js";
|
|
14
14
|
import { taskArtifactPath } from "./task-paths.js";
|
|
15
15
|
|
|
@@ -37,16 +37,11 @@ function continuityError(code, message, artifacts = [CONTINUITY_PATH]) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function nonEmptyString(value, label, maxLength) {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
try {
|
|
41
|
+
return normalizePortableText(value, { label, maxLength });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw continuityError("E_CONTINUITY_INVALID", error.message);
|
|
42
44
|
}
|
|
43
|
-
if (value.length > maxLength) {
|
|
44
|
-
throw continuityError("E_CONTINUITY_INVALID", `${label} exceeds the ${maxLength}-character limit`);
|
|
45
|
-
}
|
|
46
|
-
if (/\p{Cc}/u.test(value)) {
|
|
47
|
-
throw continuityError("E_CONTINUITY_INVALID", `${label} contains control characters`);
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
function fingerprint(value, label) {
|
|
@@ -171,7 +166,11 @@ export function assertContinuitySemantics(input) {
|
|
|
171
166
|
? { resumeNote: nonEmptyString(input.resumeNote, "resumeNote", LIMITS.resumeNote) }
|
|
172
167
|
: {}),
|
|
173
168
|
};
|
|
174
|
-
|
|
169
|
+
try {
|
|
170
|
+
assertPortableContextSafe(normalized);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw continuityError("E_CONTINUITY_INVALID", error.message);
|
|
173
|
+
}
|
|
175
174
|
return normalized;
|
|
176
175
|
}
|
|
177
176
|
|
package/src/core/error-codes.js
CHANGED
|
@@ -83,6 +83,70 @@ export const E_EXECUTION_PROFILE_SAFETY_FLOOR_INVALID = "E_EXECUTION_PROFILE_SAF
|
|
|
83
83
|
export const E_USAGE_INVALID = "E_USAGE_INVALID";
|
|
84
84
|
export const E_USAGE_SOURCE_INVALID = "E_USAGE_SOURCE_INVALID";
|
|
85
85
|
export const E_EFFICIENCY_BASELINE_INVALID = "E_EFFICIENCY_BASELINE_INVALID";
|
|
86
|
+
export const E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID = "E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID";
|
|
87
|
+
export const E_STRUCTURAL_QUALITY_PROVIDER_INVALID = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
|
|
88
|
+
export const E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE = "E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE";
|
|
89
|
+
export const E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED = "E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED";
|
|
90
|
+
export const E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID = "E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID";
|
|
91
|
+
export const E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID = "E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID";
|
|
92
|
+
export const E_STRUCTURAL_QUALITY_SCAN_FAILED = "E_STRUCTURAL_QUALITY_SCAN_FAILED";
|
|
93
|
+
export const E_STRUCTURAL_QUALITY_TIMEOUT = "E_STRUCTURAL_QUALITY_TIMEOUT";
|
|
94
|
+
export const E_STRUCTURAL_QUALITY_OUTPUT_LIMIT = "E_STRUCTURAL_QUALITY_OUTPUT_LIMIT";
|
|
95
|
+
export const E_STRUCTURAL_QUALITY_BASELINE_MISSING = "E_STRUCTURAL_QUALITY_BASELINE_MISSING";
|
|
96
|
+
export const E_STRUCTURAL_QUALITY_BASELINE_EXISTS = "E_STRUCTURAL_QUALITY_BASELINE_EXISTS";
|
|
97
|
+
export const E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID = "E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID";
|
|
98
|
+
export const E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH = "E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH";
|
|
99
|
+
export const E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE = "E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE";
|
|
100
|
+
export const E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH = "E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH";
|
|
101
|
+
export const E_STRUCTURAL_QUALITY_EVIDENCE_STALE = "E_STRUCTURAL_QUALITY_EVIDENCE_STALE";
|
|
102
|
+
export const E_STRUCTURAL_QUALITY_SOURCE_DRIFT = "E_STRUCTURAL_QUALITY_SOURCE_DRIFT";
|
|
103
|
+
export const E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE = "E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE";
|
|
104
|
+
export const E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE = "E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE";
|
|
105
|
+
export const E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE = "E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE";
|
|
106
|
+
export const E_STRUCTURAL_QUALITY_REGRESSION = "E_STRUCTURAL_QUALITY_REGRESSION";
|
|
107
|
+
|
|
108
|
+
export const E_ADVISORY_CONTEXT_PROVIDER_INVALID = "E_ADVISORY_CONTEXT_PROVIDER_INVALID";
|
|
109
|
+
export const E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE = "E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE";
|
|
110
|
+
export const E_ADVISORY_CONTEXT_QUERY_INVALID = "E_ADVISORY_CONTEXT_QUERY_INVALID";
|
|
111
|
+
export const E_ADVISORY_CONTEXT_REQUEST_INVALID = "E_ADVISORY_CONTEXT_REQUEST_INVALID";
|
|
112
|
+
export const E_ADVISORY_CONTEXT_RESULT_INVALID = "E_ADVISORY_CONTEXT_RESULT_INVALID";
|
|
113
|
+
export const E_ADVISORY_CONTEXT_TIMEOUT = "E_ADVISORY_CONTEXT_TIMEOUT";
|
|
114
|
+
export const E_ADVISORY_CONTEXT_OUTPUT_LIMIT = "E_ADVISORY_CONTEXT_OUTPUT_LIMIT";
|
|
115
|
+
export const E_PORTABLE_CONTEXT_INVALID = "E_PORTABLE_CONTEXT_INVALID";
|
|
116
|
+
export const E_HANDOFF_ACCEPTANCE_UNBOUND = "E_HANDOFF_ACCEPTANCE_UNBOUND";
|
|
117
|
+
export const E_HANDOFF_STALE = "E_HANDOFF_STALE";
|
|
118
|
+
export const E_HANDOFF_ALREADY_ACCEPTED = "E_HANDOFF_ALREADY_ACCEPTED";
|
|
119
|
+
export const E_HANDOFF_ACCEPTANCE_INCONSISTENT = "E_HANDOFF_ACCEPTANCE_INCONSISTENT";
|
|
120
|
+
|
|
121
|
+
const STRUCTURAL_QUALITY_ERROR_METADATA = Object.freeze(Object.fromEntries([
|
|
122
|
+
[E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, "Correct structuralQuality mode, provider ID, budgets, floors, or optimization limits in .forgeloop/config.json."],
|
|
123
|
+
[E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Use a provider implementing id, detect(input), and scan(input) with the documented normalized boundary."],
|
|
124
|
+
[E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE, "Install or expose the requested provider outside ForgeLoop, or use observe mode and record the limitation; ForgeLoop never auto-installs it."],
|
|
125
|
+
[E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED, "Use a verified Sentrux version (0.5.5, 0.5.6, or 0.5.7), or select a compatible provider through trusted runtime context."],
|
|
126
|
+
[E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID, "Repair the provider MCP handshake or response shape; malformed external data cannot become evidence."],
|
|
127
|
+
[E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID, "Ensure the provider exposes the required scan and health tool argument schemas."],
|
|
128
|
+
[E_STRUCTURAL_QUALITY_SCAN_FAILED, "Inspect the provider failure and rerun quality-baseline or quality-verify after the external analyzer is healthy."],
|
|
129
|
+
[E_STRUCTURAL_QUALITY_TIMEOUT, "Use a responsive provider or a bounded timeout within the supported limit; never promote a timed-out scan."],
|
|
130
|
+
[E_STRUCTURAL_QUALITY_OUTPUT_LIMIT, "Reduce provider output or diagnostics; the 2 MiB combined process-output limit is fail-closed."],
|
|
131
|
+
[E_STRUCTURAL_QUALITY_BASELINE_MISSING, "Run forgeloop quality-baseline --task <id> after PLANNED and before EXECUTING."],
|
|
132
|
+
[E_STRUCTURAL_QUALITY_BASELINE_EXISTS, "Use the existing immutable baseline or request --replace while the task is still before EXECUTING."],
|
|
133
|
+
[E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID, "Baseline replacement is forbidden after EXECUTING; repair the current task against its original baseline."],
|
|
134
|
+
[E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Reconcile contract, route, policy, scope, provider, or rules drift before using the baseline."],
|
|
135
|
+
[E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE, "Restore the baseline provider/version/rules/policy/scope bindings and rerun quality-verify."],
|
|
136
|
+
[E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH, "Use a compatible measurement model and provider across baseline and evaluation observations."],
|
|
137
|
+
[E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Rerun quality-verify in the active verification cycle and refresh completion through the canonical receipt pipeline."],
|
|
138
|
+
[E_STRUCTURAL_QUALITY_SOURCE_DRIFT, "Ensure the worktree is not mutated during provider observation and rerun quality-baseline or quality-verify."],
|
|
139
|
+
[E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE, "Repair unreadable or unsafe source material before structural-quality evidence can be trusted."],
|
|
140
|
+
[E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE, "Rerun quality-verify under the active task epoch without concurrent state mutations."],
|
|
141
|
+
[E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE, "Rerun quality-verify to reconcile and project the canonical check from the existing evaluation artifact."],
|
|
142
|
+
[E_STRUCTURAL_QUALITY_REGRESSION, "Use the bottleneck and root-cause deltas to record an evidence-backed diagnosis, correct within scope, and verify a new cycle."],
|
|
143
|
+
].map(([code, safeResolution]) => [code, Object.freeze({
|
|
144
|
+
code,
|
|
145
|
+
category: "structural-quality",
|
|
146
|
+
classification: "PUBLIC_STABLE",
|
|
147
|
+
meaning: "Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary.",
|
|
148
|
+
safeResolution,
|
|
149
|
+
})])));
|
|
86
150
|
|
|
87
151
|
const EXTENSION_PUBLIC_ERROR_CODES = Object.freeze(Object.fromEntries([
|
|
88
152
|
E_WORKSPACE_IDENTITY_UNAVAILABLE,
|
|
@@ -222,11 +286,100 @@ export const E_APPROVAL_ALREADY_RESOLVED = "E_APPROVAL_ALREADY_RESOLVED";
|
|
|
222
286
|
export const E_TRAJECTORY_SCENARIO_INVALID = "E_TRAJECTORY_SCENARIO_INVALID";
|
|
223
287
|
export const E_TRAJECTORY_REFERENCE_REQUIRED = "E_TRAJECTORY_REFERENCE_REQUIRED";
|
|
224
288
|
|
|
289
|
+
const ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA = Object.freeze(Object.fromEntries([
|
|
290
|
+
[E_ADVISORY_CONTEXT_PROVIDER_INVALID, Object.freeze({
|
|
291
|
+
code: E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
292
|
+
category: "advisory-context",
|
|
293
|
+
classification: "PUBLIC_STABLE",
|
|
294
|
+
meaning: "Advisory context provider configuration or interface implementation is invalid.",
|
|
295
|
+
safeResolution: "Use a provider implementing id, recall(input) with bounded query parameters; advisory context is optional.",
|
|
296
|
+
})],
|
|
297
|
+
[E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, Object.freeze({
|
|
298
|
+
code: E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
299
|
+
category: "advisory-context",
|
|
300
|
+
classification: "PUBLIC_STABLE",
|
|
301
|
+
meaning: "Requested advisory context provider is not registered in runtime context.",
|
|
302
|
+
safeResolution: "Register the provider in runtime context before recall, or proceed without advisory context; provider failure never blocks canonical lifecycle.",
|
|
303
|
+
})],
|
|
304
|
+
[E_ADVISORY_CONTEXT_QUERY_INVALID, Object.freeze({
|
|
305
|
+
code: E_ADVISORY_CONTEXT_QUERY_INVALID,
|
|
306
|
+
category: "advisory-context",
|
|
307
|
+
classification: "PUBLIC_STABLE",
|
|
308
|
+
meaning: "Advisory context query failed portable-context validation or exceeded budget.",
|
|
309
|
+
safeResolution: "Provide a bounded query free of control characters and secret-like values.",
|
|
310
|
+
})],
|
|
311
|
+
[E_ADVISORY_CONTEXT_REQUEST_INVALID, Object.freeze({
|
|
312
|
+
code: E_ADVISORY_CONTEXT_REQUEST_INVALID,
|
|
313
|
+
category: "advisory-context",
|
|
314
|
+
classification: "PUBLIC_STABLE",
|
|
315
|
+
meaning: "Advisory context recall budgets are not finite integer values within the supported request contract.",
|
|
316
|
+
safeResolution: "Provide finite integer limit, maxItemChars, maxTotalChars, and timeoutMs values; oversized valid values are clamped to documented maxima.",
|
|
317
|
+
})],
|
|
318
|
+
[E_ADVISORY_CONTEXT_RESULT_INVALID, Object.freeze({
|
|
319
|
+
code: E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
320
|
+
category: "advisory-context",
|
|
321
|
+
classification: "PUBLIC_STABLE",
|
|
322
|
+
meaning: "Advisory context provider returned an invalid result structure.",
|
|
323
|
+
safeResolution: "Ensure provider returns items with string summary and optional title, sourceRef, observedAt, confidence.",
|
|
324
|
+
})],
|
|
325
|
+
[E_ADVISORY_CONTEXT_TIMEOUT, Object.freeze({
|
|
326
|
+
code: E_ADVISORY_CONTEXT_TIMEOUT,
|
|
327
|
+
category: "advisory-context",
|
|
328
|
+
classification: "PUBLIC_STABLE",
|
|
329
|
+
meaning: "Advisory context recall exceeded its execution timeout.",
|
|
330
|
+
safeResolution: "Use a responsive provider or increase timeout within limits; advisory context is optional.",
|
|
331
|
+
})],
|
|
332
|
+
[E_ADVISORY_CONTEXT_OUTPUT_LIMIT, Object.freeze({
|
|
333
|
+
code: E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
334
|
+
category: "advisory-context",
|
|
335
|
+
classification: "PUBLIC_STABLE",
|
|
336
|
+
meaning: "Advisory context output exceeded the configured character or item limit.",
|
|
337
|
+
safeResolution: "Reduce query scope, limit items, or truncate oversized summaries at the provider.",
|
|
338
|
+
})],
|
|
339
|
+
[E_PORTABLE_CONTEXT_INVALID, Object.freeze({
|
|
340
|
+
code: E_PORTABLE_CONTEXT_INVALID,
|
|
341
|
+
category: "portable-context",
|
|
342
|
+
classification: "PUBLIC_STABLE",
|
|
343
|
+
meaning: "Text or object failed portable-context safety, character, or secret limits.",
|
|
344
|
+
safeResolution: "Ensure text is bounded, contains no control characters, and contains no secret-like values.",
|
|
345
|
+
})],
|
|
346
|
+
[E_HANDOFF_ACCEPTANCE_UNBOUND, Object.freeze({
|
|
347
|
+
code: E_HANDOFF_ACCEPTANCE_UNBOUND,
|
|
348
|
+
category: "handoff",
|
|
349
|
+
classification: "PUBLIC_STABLE",
|
|
350
|
+
meaning: "Handoff snapshot lacks required workStateFingerprint binding.",
|
|
351
|
+
safeResolution: "Create a fresh handoff from the current ForgeLoop version before accepting it.",
|
|
352
|
+
})],
|
|
353
|
+
[E_HANDOFF_STALE, Object.freeze({
|
|
354
|
+
code: E_HANDOFF_STALE,
|
|
355
|
+
category: "handoff",
|
|
356
|
+
classification: "PUBLIC_STABLE",
|
|
357
|
+
meaning: "Handoff snapshot has drifted from the current canonical task state or repository.",
|
|
358
|
+
safeResolution: "Create a new fresh handoff from the current task state instead of accepting a stale snapshot.",
|
|
359
|
+
})],
|
|
360
|
+
[E_HANDOFF_ALREADY_ACCEPTED, Object.freeze({
|
|
361
|
+
code: E_HANDOFF_ALREADY_ACCEPTED,
|
|
362
|
+
category: "handoff",
|
|
363
|
+
classification: "PUBLIC_STABLE",
|
|
364
|
+
meaning: "Handoff was already accepted by a different consumer.",
|
|
365
|
+
safeResolution: "Create a new handoff for the new consumer; do not manually edit acceptance events.",
|
|
366
|
+
})],
|
|
367
|
+
[E_HANDOFF_ACCEPTANCE_INCONSISTENT, Object.freeze({
|
|
368
|
+
code: E_HANDOFF_ACCEPTANCE_INCONSISTENT,
|
|
369
|
+
category: "handoff",
|
|
370
|
+
classification: "PUBLIC_STABLE",
|
|
371
|
+
meaning: "Handoff acceptance disagrees with task event ledger history.",
|
|
372
|
+
safeResolution: "Verify ledger integrity and require a preceding valid HANDOFF_CREATED event.",
|
|
373
|
+
})],
|
|
374
|
+
]));
|
|
375
|
+
|
|
225
376
|
/**
|
|
226
377
|
* Public, stable ForgeLoop error and reason codes documented for users and harnesses.
|
|
227
378
|
*/
|
|
228
379
|
export const PUBLIC_ERROR_CODES = Object.freeze({
|
|
229
380
|
...EXTENSION_PUBLIC_ERROR_CODES,
|
|
381
|
+
...STRUCTURAL_QUALITY_ERROR_METADATA,
|
|
382
|
+
...ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA,
|
|
230
383
|
E_PREFLIGHT_NOT_READY: Object.freeze({
|
|
231
384
|
code: "E_PREFLIGHT_NOT_READY",
|
|
232
385
|
category: "preflight",
|
|
@@ -1044,6 +1197,27 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
|
|
|
1044
1197
|
E_USAGE_INVALID,
|
|
1045
1198
|
E_USAGE_SOURCE_INVALID,
|
|
1046
1199
|
E_EFFICIENCY_BASELINE_INVALID,
|
|
1200
|
+
E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID,
|
|
1201
|
+
E_STRUCTURAL_QUALITY_PROVIDER_INVALID,
|
|
1202
|
+
E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
|
|
1203
|
+
E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED,
|
|
1204
|
+
E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID,
|
|
1205
|
+
E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID,
|
|
1206
|
+
E_STRUCTURAL_QUALITY_SCAN_FAILED,
|
|
1207
|
+
E_STRUCTURAL_QUALITY_TIMEOUT,
|
|
1208
|
+
E_STRUCTURAL_QUALITY_OUTPUT_LIMIT,
|
|
1209
|
+
E_STRUCTURAL_QUALITY_BASELINE_MISSING,
|
|
1210
|
+
E_STRUCTURAL_QUALITY_BASELINE_EXISTS,
|
|
1211
|
+
E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID,
|
|
1212
|
+
E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH,
|
|
1213
|
+
E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE,
|
|
1214
|
+
E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH,
|
|
1215
|
+
E_STRUCTURAL_QUALITY_EVIDENCE_STALE,
|
|
1216
|
+
E_STRUCTURAL_QUALITY_SOURCE_DRIFT,
|
|
1217
|
+
E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE,
|
|
1218
|
+
E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE,
|
|
1219
|
+
E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE,
|
|
1220
|
+
E_STRUCTURAL_QUALITY_REGRESSION,
|
|
1047
1221
|
E_RECONCILE_NOT_STALE,
|
|
1048
1222
|
E_RECONCILE_PHASE_INVALID,
|
|
1049
1223
|
E_RECONCILE_UNSUPPORTED_DRIFT,
|
|
@@ -1132,6 +1306,18 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
|
|
|
1132
1306
|
E_FAILURE_SIGNATURE_INVALID,
|
|
1133
1307
|
E_STRATEGY_OSCILLATION,
|
|
1134
1308
|
"E_TRACE_SNAPSHOT_INCONSISTENT",
|
|
1309
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
1310
|
+
E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
1311
|
+
E_ADVISORY_CONTEXT_QUERY_INVALID,
|
|
1312
|
+
E_ADVISORY_CONTEXT_REQUEST_INVALID,
|
|
1313
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
1314
|
+
E_ADVISORY_CONTEXT_TIMEOUT,
|
|
1315
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
1316
|
+
E_PORTABLE_CONTEXT_INVALID,
|
|
1317
|
+
E_HANDOFF_ACCEPTANCE_UNBOUND,
|
|
1318
|
+
E_HANDOFF_STALE,
|
|
1319
|
+
E_HANDOFF_ALREADY_ACCEPTED,
|
|
1320
|
+
E_HANDOFF_ACCEPTANCE_INCONSISTENT,
|
|
1135
1321
|
]));
|
|
1136
1322
|
|
|
1137
1323
|
/**
|
package/src/core/events.js
CHANGED
|
@@ -160,6 +160,16 @@ export function validateKnownEventDetails(event) {
|
|
|
160
160
|
}
|
|
161
161
|
assertFingerprint(event.details.digest, "HANDOFF_CREATED details.digest");
|
|
162
162
|
return;
|
|
163
|
+
case "HANDOFF_ACCEPTED":
|
|
164
|
+
if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)
|
|
165
|
+
|| typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
|
|
166
|
+
|| typeof event.details.handoffDigest !== "string"
|
|
167
|
+
|| typeof event.details.consumerId !== "string" || !event.details.consumerId.trim()
|
|
168
|
+
|| (event.details.harness !== undefined && (typeof event.details.harness !== "string" || !event.details.harness.trim()))) {
|
|
169
|
+
throw protocolError("E_EVENT_INVALID", "HANDOFF_ACCEPTED requires a valid handoffId, handoffDigest, and consumerId");
|
|
170
|
+
}
|
|
171
|
+
assertFingerprint(event.details.handoffDigest, "HANDOFF_ACCEPTED details.handoffDigest");
|
|
172
|
+
return;
|
|
163
173
|
case "RESPONSIBILITY_SET":
|
|
164
174
|
assertStructuredArtifactEvent(event, ["responsibilityFingerprint"], "RESPONSIBILITY_SET");
|
|
165
175
|
if (typeof event.details.label !== "string" || !event.details.label) {
|
|
@@ -513,6 +523,8 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
513
523
|
const seen = new Set();
|
|
514
524
|
let lastMilestone = -1;
|
|
515
525
|
const milestoneCounts = new Map();
|
|
526
|
+
const createdHandoffs = new Map();
|
|
527
|
+
const acceptedHandoffs = new Set();
|
|
516
528
|
for (const [index, event] of events.entries()) {
|
|
517
529
|
if (event.seq !== index + 1) {
|
|
518
530
|
errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
|
|
@@ -584,6 +596,26 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
584
596
|
if (event.event === "COMPLETION_REJECTED" && !seen.has("VERIFICATION_STARTED")) {
|
|
585
597
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "completion rejected before verification started" });
|
|
586
598
|
}
|
|
599
|
+
if (event.event === "HANDOFF_CREATED") {
|
|
600
|
+
if (event.details?.handoffId) {
|
|
601
|
+
createdHandoffs.set(event.details.handoffId, event.details.digest);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (event.event === "HANDOFF_ACCEPTED") {
|
|
605
|
+
const hId = event.details?.handoffId;
|
|
606
|
+
if (hId) {
|
|
607
|
+
if (acceptedHandoffs.has(hId)) {
|
|
608
|
+
errors.push({ code: "E_HANDOFF_ALREADY_ACCEPTED", message: `duplicate HANDOFF_ACCEPTED for handoffId ${hId}` });
|
|
609
|
+
} else {
|
|
610
|
+
acceptedHandoffs.add(hId);
|
|
611
|
+
if (!createdHandoffs.has(hId)) {
|
|
612
|
+
errors.push({ code: "E_HANDOFF_ACCEPTANCE_INCONSISTENT", message: `HANDOFF_ACCEPTED has no preceding HANDOFF_CREATED for handoffId ${hId}` });
|
|
613
|
+
} else if (createdHandoffs.get(hId) !== event.details?.handoffDigest) {
|
|
614
|
+
errors.push({ code: "E_HANDOFF_ACCEPTANCE_INCONSISTENT", message: `HANDOFF_ACCEPTED digest mismatch for handoffId ${hId}` });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
587
619
|
}
|
|
588
620
|
validateLegacyRecoveryMigrations(events, errors, {
|
|
589
621
|
allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
|
|
@@ -112,6 +112,7 @@ export function projectExecutionProfileContext({
|
|
|
112
112
|
route,
|
|
113
113
|
state,
|
|
114
114
|
nextAction,
|
|
115
|
+
runtimeContext,
|
|
115
116
|
} = {}) {
|
|
116
117
|
if (typeof taskId !== "string" || !taskId) throw contextError("E_TASK_REQUIRED", "taskId is required");
|
|
117
118
|
if (!contract || typeof contract !== "object" || Array.isArray(contract)) {
|
|
@@ -126,6 +127,18 @@ export function projectExecutionProfileContext({
|
|
|
126
127
|
const profile = route.executionProfile ? assertExecutionProfile(route.executionProfile) : legacyExecutionProfile();
|
|
127
128
|
const resolvedProfile = profile.resolved;
|
|
128
129
|
const policy = getExecutionProfileContextPolicy(resolvedProfile);
|
|
130
|
+
const available = [...OPTIONAL_CONTEXT_BY_PROFILE[resolvedProfile]];
|
|
131
|
+
const advisoryProviders = runtimeContext?.advisoryContextProviders;
|
|
132
|
+
const hasAdvisory = Boolean(
|
|
133
|
+
advisoryProviders && (
|
|
134
|
+
advisoryProviders instanceof Map
|
|
135
|
+
? advisoryProviders.size > 0
|
|
136
|
+
: Object.keys(advisoryProviders).length > 0
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
if (hasAdvisory && !available.includes("advisory-context")) {
|
|
140
|
+
available.push("advisory-context");
|
|
141
|
+
}
|
|
129
142
|
return {
|
|
130
143
|
schemaVersion: 1,
|
|
131
144
|
protocolVersion: 1,
|
|
@@ -140,7 +153,7 @@ export function projectExecutionProfileContext({
|
|
|
140
153
|
verificationRequirements: verificationRequirements(contract),
|
|
141
154
|
contextPolicy: policy,
|
|
142
155
|
optionalContext: {
|
|
143
|
-
available
|
|
156
|
+
available,
|
|
144
157
|
loaded: [],
|
|
145
158
|
},
|
|
146
159
|
invariants: { ...PROFILE_INVARIANTS },
|
|
@@ -173,5 +186,6 @@ export async function buildExecutionProfileContext({
|
|
|
173
186
|
route: routeArtifact.value,
|
|
174
187
|
state,
|
|
175
188
|
nextAction,
|
|
189
|
+
runtimeContext,
|
|
176
190
|
});
|
|
177
191
|
}
|
package/src/core/filesystem.js
CHANGED
|
@@ -73,6 +73,38 @@ export function ensureWithin(root, relativePath) {
|
|
|
73
73
|
return path.join(root, normalized);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function normalizeWindowsPathForComparison(value) {
|
|
77
|
+
const lowerValue = value.toLowerCase();
|
|
78
|
+
let normalized = value;
|
|
79
|
+
if (lowerValue.startsWith("\\\\?\\unc\\")) {
|
|
80
|
+
normalized = `\\\\${value.slice(8)}`;
|
|
81
|
+
} else if (lowerValue.startsWith("\\\\.\\unc\\")) {
|
|
82
|
+
normalized = `\\\\${value.slice(8)}`;
|
|
83
|
+
} else if (lowerValue.startsWith("\\\\?\\")) {
|
|
84
|
+
normalized = value.slice(4);
|
|
85
|
+
} else if (lowerValue.startsWith("\\\\.\\")) {
|
|
86
|
+
normalized = value.slice(4);
|
|
87
|
+
}
|
|
88
|
+
return path.win32.normalize(normalized).toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isPathWithin(root, candidate, { platform = process.platform } = {}) {
|
|
92
|
+
const pathApi = platform === "win32" ? path.win32 : path;
|
|
93
|
+
const normalizeForComparison = (value) => {
|
|
94
|
+
return platform === "win32"
|
|
95
|
+
? normalizeWindowsPathForComparison(value)
|
|
96
|
+
: pathApi.normalize(value);
|
|
97
|
+
};
|
|
98
|
+
const relative = pathApi.relative(
|
|
99
|
+
normalizeForComparison(root),
|
|
100
|
+
normalizeForComparison(candidate),
|
|
101
|
+
);
|
|
102
|
+
return relative === ""
|
|
103
|
+
|| (relative !== ".."
|
|
104
|
+
&& !relative.startsWith(`..${pathApi.sep}`)
|
|
105
|
+
&& !pathApi.isAbsolute(relative));
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
export async function assertSafePath(root, relativePath) {
|
|
77
109
|
const destination = ensureWithin(root, relativePath);
|
|
78
110
|
const absoluteRoot = path.resolve(root);
|
|
@@ -105,8 +137,7 @@ export async function assertSafePath(root, relativePath) {
|
|
|
105
137
|
}
|
|
106
138
|
const resolvedRoot = await realpathWithTransientWindowsRetry(absoluteRoot);
|
|
107
139
|
const resolvedExisting = await realpathWithTransientWindowsRetry(existing);
|
|
108
|
-
|
|
109
|
-
if (relativeResolved === ".." || relativeResolved.startsWith(`..${path.sep}`) || path.isAbsolute(relativeResolved)) {
|
|
140
|
+
if (!isPathWithin(resolvedRoot, resolvedExisting)) {
|
|
110
141
|
throw new Error(`Path escapes target directory: ${relativePath}`);
|
|
111
142
|
}
|
|
112
143
|
return destination;
|
|
@@ -186,4 +217,4 @@ export async function writeFileAtomic(filePath, bytes, { dryRun = false } = {})
|
|
|
186
217
|
}
|
|
187
218
|
throw error;
|
|
188
219
|
}
|
|
189
|
-
}
|
|
220
|
+
}
|