@cassiomc1/forgeloop 1.5.0 → 1.6.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/DOCS_INDEX.md +13 -8
- package/EXECUTION_STATE.md +20 -0
- package/LOOP_ENGINEERING.md +81 -0
- package/LOOP_SYSTEM_DESIGN.md +32 -0
- package/PROTOCOL_INTEGRATION.md +46 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +31 -9
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +39 -0
- package/docs/ARTIFACT_REFERENCE.md +140 -0
- package/docs/CLI_REFERENCE.md +294 -3
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +22 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/MCP.md +33 -0
- package/docs/RECIPES.md +67 -0
- package/docs/TROUBLESHOOTING.md +106 -2
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +8 -6
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +94 -0
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +49 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +21 -13
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +48 -0
- package/src/core/audit.js +25 -0
- package/src/core/bundles.js +15 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +210 -1
- package/src/core/command-executors.js +171 -15
- package/src/core/command-runtime.js +12 -1
- package/src/core/completion-artifacts.js +37 -12
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +343 -0
- package/src/core/events.js +41 -1
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +105 -7
- package/src/core/integration-invocation-policy.js +47 -0
- package/src/core/integration-resources.js +51 -0
- package/src/core/next-action-model.js +35 -1
- package/src/core/next-action.js +459 -3
- package/src/core/phase.js +40 -21
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/protocol-info.js +48 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +15 -12
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +8 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-paths.js +28 -0
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +8 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +8 -0
- package/src/core/work-state.js +10 -5
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { recordDiagnosis } from "../core/diagnosis.js";
|
|
2
|
+
import { recordStructuredDiagnosticCase } from "../core/diagnostic-record.js";
|
|
2
3
|
import { withTaskMutation } from "../core/task-command.js";
|
|
3
4
|
|
|
4
|
-
export { recordDiagnosis };
|
|
5
|
+
export { recordDiagnosis, recordStructuredDiagnosticCase };
|
|
5
6
|
|
|
6
7
|
export async function runRecordDiagnosis({
|
|
7
8
|
target,
|
|
8
9
|
packageRoot,
|
|
10
|
+
file = null,
|
|
9
11
|
hypothesis,
|
|
10
12
|
failureClass,
|
|
11
13
|
evidenceRefs = [],
|
|
@@ -15,6 +17,26 @@ export async function runRecordDiagnosis({
|
|
|
15
17
|
taskId,
|
|
16
18
|
task,
|
|
17
19
|
}) {
|
|
20
|
+
if (file) {
|
|
21
|
+
const legacyFields = [hypothesis, failureClass, settledBy, nextSafeAction].some((value) => value !== undefined && value !== null)
|
|
22
|
+
|| (Array.isArray(evidenceRefs) && evidenceRefs.length > 0)
|
|
23
|
+
|| Boolean(evidenceRef);
|
|
24
|
+
if (legacyFields) {
|
|
25
|
+
const error = new Error("record-diagnosis accepts either --file (structured case) or legacy diagnosis fields, not both");
|
|
26
|
+
error.code = "E_INPUT_CONFLICT";
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "record-diagnosis", async (ctx) => {
|
|
30
|
+
return recordStructuredDiagnosticCase({
|
|
31
|
+
target,
|
|
32
|
+
packageRoot,
|
|
33
|
+
caseFile: file,
|
|
34
|
+
caseInput: null,
|
|
35
|
+
taskId: ctx?.taskId ?? null,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
const refs = Array.isArray(evidenceRefs) && evidenceRefs.length > 0
|
|
19
41
|
? evidenceRefs
|
|
20
42
|
: (evidenceRef ? [evidenceRef] : []);
|
|
@@ -34,6 +56,20 @@ export async function runRecordDiagnosis({
|
|
|
34
56
|
}
|
|
35
57
|
|
|
36
58
|
export function formatRecordDiagnosisResult(result) {
|
|
59
|
+
if (result.diagnosticCase) {
|
|
60
|
+
const d = result.diagnosticCase;
|
|
61
|
+
return [
|
|
62
|
+
`FORGELOOP DIAGNOSTIC CASE RECORDED`,
|
|
63
|
+
`CYCLE: ${d.verificationCycle}`,
|
|
64
|
+
`REVISION: ${d.diagnosticRevision}`,
|
|
65
|
+
`FAILURE CLASS: ${d.failureClass}`,
|
|
66
|
+
`OBSERVATIONS: ${(d.observations ?? []).length}`,
|
|
67
|
+
`CONTRIBUTORS: ${(d.contributors ?? []).length}`,
|
|
68
|
+
`HYPOTHESES: ${(d.hypotheses ?? []).map((hypothesis) => hypothesis.id).join(", ")}`,
|
|
69
|
+
`NEXT SAFE ACTION: ${d.nextSafeAction?.statement ?? ""}`,
|
|
70
|
+
`FINGERPRINT: ${d.diagnosticFingerprint}`,
|
|
71
|
+
].join("\n") + "\n";
|
|
72
|
+
}
|
|
37
73
|
const d = result.diagnosis ?? result.event?.details ?? {};
|
|
38
74
|
return [
|
|
39
75
|
`FORGELOOP DIAGNOSIS RECORDED`,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { recordHypothesisDisposition } from "../core/diagnostic-record.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
export { recordHypothesisDisposition };
|
|
5
|
+
|
|
6
|
+
export async function runRecordHypothesisDisposition({
|
|
7
|
+
target,
|
|
8
|
+
packageRoot,
|
|
9
|
+
hypothesis,
|
|
10
|
+
hypothesisRef = null,
|
|
11
|
+
status,
|
|
12
|
+
evidenceRefs = [],
|
|
13
|
+
evidenceRef = null,
|
|
14
|
+
reason,
|
|
15
|
+
taskId,
|
|
16
|
+
task,
|
|
17
|
+
}) {
|
|
18
|
+
const refs = Array.isArray(evidenceRefs) && evidenceRefs.length > 0
|
|
19
|
+
? evidenceRefs
|
|
20
|
+
: (evidenceRef ? [evidenceRef] : []);
|
|
21
|
+
|
|
22
|
+
return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "record-hypothesis-disposition", async (ctx) => {
|
|
23
|
+
return recordHypothesisDisposition({
|
|
24
|
+
target,
|
|
25
|
+
packageRoot,
|
|
26
|
+
hypothesisRef: hypothesis ?? hypothesisRef,
|
|
27
|
+
status,
|
|
28
|
+
evidenceRefs: refs,
|
|
29
|
+
reason,
|
|
30
|
+
taskId: ctx?.taskId ?? null,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatRecordHypothesisDispositionResult(result) {
|
|
36
|
+
const disposition = result.disposition ?? {};
|
|
37
|
+
return [
|
|
38
|
+
"FORGELOOP HYPOTHESIS DISPOSITION RECORDED",
|
|
39
|
+
`CYCLE: ${disposition.verificationCycle}`,
|
|
40
|
+
`HYPOTHESIS: ${disposition.hypothesisRef}`,
|
|
41
|
+
`STATUS: ${disposition.status}`,
|
|
42
|
+
`EVIDENCE: ${(disposition.evidenceRefs ?? []).join(", ")}`,
|
|
43
|
+
`REASON: ${disposition.reason}`,
|
|
44
|
+
].join("\n") + "\n";
|
|
45
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { recordIntervention } from "../core/diagnostic-record.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
export { recordIntervention };
|
|
5
|
+
|
|
6
|
+
export async function runRecordIntervention({
|
|
7
|
+
target,
|
|
8
|
+
packageRoot,
|
|
9
|
+
file = null,
|
|
10
|
+
taskId,
|
|
11
|
+
task,
|
|
12
|
+
}) {
|
|
13
|
+
return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "record-intervention", async (ctx) => {
|
|
14
|
+
return recordIntervention({
|
|
15
|
+
target,
|
|
16
|
+
packageRoot,
|
|
17
|
+
interventionFile: file,
|
|
18
|
+
interventionInput: null,
|
|
19
|
+
taskId: ctx?.taskId ?? null,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatRecordInterventionResult(result) {
|
|
25
|
+
const intervention = result.intervention?.intervention ?? {};
|
|
26
|
+
return [
|
|
27
|
+
"FORGELOOP INTERVENTION RECORDED",
|
|
28
|
+
`CYCLE: ${result.intervention?.verificationCycle}`,
|
|
29
|
+
`ID: ${intervention.id}`,
|
|
30
|
+
`KIND: ${intervention.kind}`,
|
|
31
|
+
`STATEMENT: ${intervention.statement}`,
|
|
32
|
+
`HYPOTHESES: ${(intervention.hypothesisRefs ?? []).join(", ")}`,
|
|
33
|
+
result.repeatedSemanticIntervention ? "WARNING: this semantic intervention has been recorded before" : null,
|
|
34
|
+
].filter((line) => line !== null).join("\n") + "\n";
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { buildTaskReflection } from "../core/reflection.js";
|
|
2
|
+
import { resolveTaskContext, TASK_SELECTION_MODES } from "../core/task-context.js";
|
|
3
|
+
|
|
4
|
+
export { buildTaskReflection };
|
|
5
|
+
|
|
6
|
+
export function formatReflectResult(result) {
|
|
7
|
+
const lines = [
|
|
8
|
+
"ForgeLoop Reflection",
|
|
9
|
+
"─".repeat(56),
|
|
10
|
+
"",
|
|
11
|
+
`Task: ${result.taskId ?? "unknown"}`,
|
|
12
|
+
`Phase: ${result.taskPhase ?? "UNKNOWN"}`,
|
|
13
|
+
`Status: ${result.status}`,
|
|
14
|
+
`Cycles: ${result.verificationCycles}`,
|
|
15
|
+
`Integrity: ${result.integrityValid ? "VALID" : "INCONSISTENT"}`,
|
|
16
|
+
"",
|
|
17
|
+
`Hypotheses: ${result.hypotheses.created} created, ${result.hypotheses.open} open, ${result.hypotheses.supported} supported, ${result.hypotheses.weakened} weakened, ${result.hypotheses.falsified} falsified`,
|
|
18
|
+
`Interventions: ${result.interventions.count} (${result.interventions.informative} informative, ${result.interventions.nonInformative} non-informative)`,
|
|
19
|
+
`Oscillation: ${result.oscillation.detected ? "DETECTED" : "none"}`,
|
|
20
|
+
result.signals.length > 0 ? `Signals: ${result.signals.join(", ")}` : "Signals: none",
|
|
21
|
+
"",
|
|
22
|
+
`Recommended protocol action: ${result.recommendedProtocolAction}`,
|
|
23
|
+
];
|
|
24
|
+
return lines.join("\n") + "\n";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runReflect({ target, packageRoot, taskId, task }) {
|
|
28
|
+
const resolved = await resolveTaskContext(target, {
|
|
29
|
+
packageRoot,
|
|
30
|
+
taskId: taskId ?? task,
|
|
31
|
+
selectionMode: TASK_SELECTION_MODES.READ,
|
|
32
|
+
});
|
|
33
|
+
return buildTaskReflection({
|
|
34
|
+
target,
|
|
35
|
+
packageRoot,
|
|
36
|
+
taskId: resolved?.taskId ?? null,
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/commands/report.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { evaluateReport } from "../core/report.js";
|
|
2
|
+
import { withResolvedTask } from "../core/task-command.js";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
+
export async function runReport(options = {}) {
|
|
5
|
+
const target = options.target;
|
|
6
|
+
const packageRoot = options.packageRoot;
|
|
7
|
+
const taskId = options.taskId ?? options.task ?? null;
|
|
8
|
+
return withResolvedTask(target, { taskId, packageRoot }, async (ctx) => {
|
|
9
|
+
return evaluateReport({ ...options, taskId: ctx?.taskId ?? null });
|
|
10
|
+
});
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
export function formatReportResult(result) {
|
|
6
14
|
const lines = ["ForgeLoop Compliance"];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { executeDurableAction } from "../core/action-execution.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
export async function runAction({ target, packageRoot, taskId, actionId, capability,
|
|
5
|
+
effectClass, actionTarget, idempotencyKey, requirement, requiredForCompletion,
|
|
6
|
+
argv, approvalId, timeoutMs, authorityContext }) {
|
|
7
|
+
return withTaskMutation(target, { taskId, packageRoot }, "run-action", async (ctx) =>
|
|
8
|
+
executeDurableAction({ target, packageRoot, taskId: ctx.taskId, argv, approvalId, timeoutMs,
|
|
9
|
+
authorityContext, input: { actionId, capability, effectClass, target: actionTarget,
|
|
10
|
+
operation: argv.join(" ").slice(0, 512), idempotencyKey,
|
|
11
|
+
requirement: requirement ?? null, requiredForCompletion: Boolean(requiredForCompletion) } }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatRunActionResult(result) {
|
|
15
|
+
return ["FORGELOOP ACTION EXECUTED", `action: ${result.action.actionId}`,
|
|
16
|
+
`state: ${result.action.state}`, `execution: ${result.execution.executionId}`,
|
|
17
|
+
`exit code: ${result.execution.exitCode ?? "not-started"}`, ""].join("\n");
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { buildTaskTrace } from "../core/trace.js";
|
|
2
|
+
import { resolveTaskContext, TASK_SELECTION_MODES } from "../core/task-context.js";
|
|
3
|
+
|
|
4
|
+
export { buildTaskTrace };
|
|
5
|
+
|
|
6
|
+
export function formatTraceResult(result) {
|
|
7
|
+
return [
|
|
8
|
+
`FORGELOOP TRACE: ${result.task.id ?? "unknown"}`,
|
|
9
|
+
`PHASE: ${result.task.phase ?? "UNKNOWN"}`,
|
|
10
|
+
`SNAPSHOT: ${result.snapshot.consistent ? "CONSISTENT" : "INCONSISTENT"} (ledger seq ${result.snapshot.ledgerTailSequence})`,
|
|
11
|
+
`INTEGRITY: ${result.integrity.valid ? "VALID" : "INCONSISTENT"}`,
|
|
12
|
+
`HISTORY QUALITY: ${result.historyQuality.level}`,
|
|
13
|
+
`EVENTS: ${result.events.length}`,
|
|
14
|
+
`CHECKS: ${result.checks.map((check) => `${check.id}=${check.currentResult} (${check.attemptCount} attempts)`).join(", ") || "none"}`,
|
|
15
|
+
`DIAGNOSTIC CASES: ${result.diagnostics.cases.length}`,
|
|
16
|
+
`INTERVENTIONS: ${result.diagnostics.interventions.length}`,
|
|
17
|
+
`DISPOSITIONS: ${result.diagnostics.dispositions.length}`,
|
|
18
|
+
"",
|
|
19
|
+
"Use --json for the full structured trace.",
|
|
20
|
+
].join("\n") + "\n";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runTrace({ target, packageRoot, taskId, task }) {
|
|
24
|
+
const resolved = await resolveTaskContext(target, {
|
|
25
|
+
packageRoot,
|
|
26
|
+
taskId: taskId ?? task,
|
|
27
|
+
selectionMode: TASK_SELECTION_MODES.READ,
|
|
28
|
+
});
|
|
29
|
+
return buildTaskTrace({
|
|
30
|
+
target,
|
|
31
|
+
packageRoot,
|
|
32
|
+
taskId: resolved?.taskId ?? null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -111,21 +111,29 @@ export async function runValidateProtocol({
|
|
|
111
111
|
await validateLoaded(loaded.find((item) => item.label === "receipt"), "execution-receipt", async (value) => validateReceipt(value, packageRoot));
|
|
112
112
|
await validateLoaded(loaded.find((item) => item.label === "continuity"), "continuity", async (value) => assertContinuitySemantics(value));
|
|
113
113
|
for (const [value, artifactPath] of [
|
|
114
|
-
[state,
|
|
115
|
-
[receipt,
|
|
114
|
+
[state, effectiveStateFile],
|
|
115
|
+
[receipt, effectiveReceiptFile],
|
|
116
116
|
]) {
|
|
117
117
|
if (!value) continue;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
try {
|
|
119
|
+
// The shared validator returns the resolved execution records when all
|
|
120
|
+
// command checks are valid and throws a canonical artifact error when a
|
|
121
|
+
// binding is invalid. Do not mistake those successful records for
|
|
122
|
+
// validation errors (which previously made task-scoped validation fail
|
|
123
|
+
// with an undefined localeCompare value).
|
|
124
|
+
await validateChecksExecutionProvenance(value.checks, {
|
|
125
|
+
target,
|
|
126
|
+
packageRoot,
|
|
127
|
+
taskId: value.taskId,
|
|
128
|
+
artifactPath,
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
schemaErrors.push({
|
|
132
|
+
code: error.code ?? "E_EXECUTION_REF_INVALID",
|
|
133
|
+
message: `Command provenance validation failed: ${error.message}`,
|
|
134
|
+
artifacts: [artifactPath, ...(error.artifacts ?? [])].filter(Boolean),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
129
137
|
}
|
|
130
138
|
for (const item of loaded.filter((candidate) => candidate.label.startsWith("task brief:"))) {
|
|
131
139
|
await validateLoaded(item, "task-brief", async (value) => validateTaskBrief(value, packageRoot));
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readAction, transitionAuthorizedAction } from "./actions.js";
|
|
2
|
+
import { loadPolicyIdentity } from "./policy-engine.js";
|
|
3
|
+
import { evaluateActionCapability } from "./capability-policy.js";
|
|
4
|
+
import {
|
|
5
|
+
E_ACTION_AUTHORIZATION_INVALID,
|
|
6
|
+
E_ACTION_INVALID,
|
|
7
|
+
E_ACTION_STATE_MISMATCH,
|
|
8
|
+
} from "./error-codes.js";
|
|
9
|
+
|
|
10
|
+
function actionAuthorizationError(code, message) {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.code = code;
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Canonical durable-action authorization.
|
|
18
|
+
*
|
|
19
|
+
* AUTHORIZED can only be produced here, after:
|
|
20
|
+
* - the action is PROPOSED with a known identity;
|
|
21
|
+
* - the capability policy, policy lock, and task policy snapshot agree
|
|
22
|
+
* (loadPolicyIdentity);
|
|
23
|
+
* - the capability decision is satisfied (ALLOW / trusted host authority /
|
|
24
|
+
* fresh HOST_ATTESTED approval);
|
|
25
|
+
* - complete policy-bound evidence is appended to the ledger event
|
|
26
|
+
* (INV-AUTH-01, INV-AUTH-02).
|
|
27
|
+
*
|
|
28
|
+
* `authorityContext` must be supplied out-of-band by a trusted host boundary;
|
|
29
|
+
* actor-controlled input can never mint it.
|
|
30
|
+
*/
|
|
31
|
+
export async function authorizeAction({
|
|
32
|
+
target,
|
|
33
|
+
packageRoot,
|
|
34
|
+
taskId,
|
|
35
|
+
actionId,
|
|
36
|
+
approvalId,
|
|
37
|
+
authorityContext,
|
|
38
|
+
}) {
|
|
39
|
+
const action = await readAction(target, { packageRoot, taskId, actionId });
|
|
40
|
+
|
|
41
|
+
if (action.state !== "PROPOSED") {
|
|
42
|
+
throw actionAuthorizationError(
|
|
43
|
+
E_ACTION_STATE_MISMATCH,
|
|
44
|
+
`action ${actionId} must be PROPOSED before authorization`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const policyIdentity = await loadPolicyIdentity(target, packageRoot, taskId);
|
|
49
|
+
|
|
50
|
+
if (policyIdentity.status !== "VALID") {
|
|
51
|
+
throw actionAuthorizationError(
|
|
52
|
+
policyIdentity.code ?? E_ACTION_AUTHORIZATION_INVALID,
|
|
53
|
+
"current capability policy is not bound to a valid task policy epoch",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const capability = await evaluateActionCapability({
|
|
58
|
+
target,
|
|
59
|
+
packageRoot,
|
|
60
|
+
action,
|
|
61
|
+
authorityContext,
|
|
62
|
+
approval: approvalId ? { approvalId } : undefined,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!capability.allowed) {
|
|
66
|
+
throw actionAuthorizationError(
|
|
67
|
+
capability.reasonCode ?? E_ACTION_INVALID,
|
|
68
|
+
`capability ${action.capability} is not authorized: ${capability.decision}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const authorization = {
|
|
73
|
+
capabilityDecision: capability.decision,
|
|
74
|
+
capabilityPolicyFingerprint: capability.policyFingerprint,
|
|
75
|
+
policyLockDigest: policyIdentity.lockDigest,
|
|
76
|
+
taskPolicyDigest: policyIdentity.taskPolicyDigest,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Decision-specific mandatory evidence (INV-AUTH-02).
|
|
80
|
+
if (capability.approval) {
|
|
81
|
+
authorization.approvalId = capability.approval.approvalId;
|
|
82
|
+
authorization.approvalFingerprint = capability.approval.approvalFingerprint;
|
|
83
|
+
authorization.authorityKind = capability.approval.authorityKind;
|
|
84
|
+
authorization.authorityRef = capability.approval.authorityRef;
|
|
85
|
+
} else if (capability.authority) {
|
|
86
|
+
authorization.authorityKind = capability.authority.authorityKind;
|
|
87
|
+
authorization.authorityRef = capability.authority.authorityRef;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const next = await transitionAuthorizedAction(target, {
|
|
91
|
+
packageRoot,
|
|
92
|
+
taskId,
|
|
93
|
+
actionId: action.actionId,
|
|
94
|
+
expectedRevision: action.revision,
|
|
95
|
+
expectedFingerprint: action.actionFingerprint,
|
|
96
|
+
details: {
|
|
97
|
+
...authorization,
|
|
98
|
+
actionFingerprint: action.actionFingerprint,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
action: next,
|
|
104
|
+
authorization,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const ACTION_EFFECT_CLASSES = Object.freeze([
|
|
2
|
+
"READ_ONLY",
|
|
3
|
+
"REVERSIBLE_WRITE",
|
|
4
|
+
"IRREVERSIBLE_WRITE",
|
|
5
|
+
"EXTERNAL_PUBLICATION",
|
|
6
|
+
"DESTRUCTIVE",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const ACTION_STATES = Object.freeze([
|
|
10
|
+
"PROPOSED",
|
|
11
|
+
"AUTHORIZED",
|
|
12
|
+
"STARTED",
|
|
13
|
+
"COMMITTED",
|
|
14
|
+
"VERIFIED",
|
|
15
|
+
"FAILED",
|
|
16
|
+
"COMMIT_UNKNOWN",
|
|
17
|
+
"CANCELLED",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const ACTION_PROVENANCE = Object.freeze([
|
|
21
|
+
"FORGELOOP_EXECUTED",
|
|
22
|
+
"HOST_ATTESTED",
|
|
23
|
+
"CALLER_REPORTED",
|
|
24
|
+
// Retained for backward compatibility with v1.6.0 artifacts. Public CLI
|
|
25
|
+
// surfaces no longer mint this provenance because caller input is not host
|
|
26
|
+
// authority.
|
|
27
|
+
"HOST_REPORTED",
|
|
28
|
+
"EXTERNAL_OBSERVED",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const TRUSTED_ACTION_PROVENANCE = Object.freeze([
|
|
32
|
+
"FORGELOOP_EXECUTED",
|
|
33
|
+
"HOST_ATTESTED",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export const ACTION_CAPABILITIES = Object.freeze([
|
|
37
|
+
"filesystem.read",
|
|
38
|
+
"filesystem.write",
|
|
39
|
+
"process.execute",
|
|
40
|
+
"dependency.install",
|
|
41
|
+
"network.read",
|
|
42
|
+
"network.write",
|
|
43
|
+
"repository.commit",
|
|
44
|
+
"repository.push",
|
|
45
|
+
"repository.pull_request",
|
|
46
|
+
"external.publish",
|
|
47
|
+
"external.delete",
|
|
48
|
+
"deployment.execute",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export const CAPABILITY_DECISIONS = Object.freeze([
|
|
52
|
+
"ALLOW",
|
|
53
|
+
"DENY",
|
|
54
|
+
"REQUIRE_AUTHORITY",
|
|
55
|
+
"REQUIRE_APPROVAL",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
export const APPROVAL_STATUSES = Object.freeze(["PENDING", "APPROVED", "REJECTED"]);
|
|
59
|
+
|
|
60
|
+
export const APPROVAL_DECISIONS = Object.freeze(["APPROVED", "REJECTED"]);
|
|
61
|
+
|
|
62
|
+
export const APPROVAL_AUTHORITY_KINDS = Object.freeze(["CALLER_ACKNOWLEDGED", "HOST_ATTESTED"]);
|
|
63
|
+
|
|
64
|
+
export const RECONCILIATION_OUTCOMES = Object.freeze([
|
|
65
|
+
"COMMITTED",
|
|
66
|
+
"NOT_COMMITTED",
|
|
67
|
+
"UNKNOWN",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
export const SIDE_EFFECTING_EFFECT_CLASSES = Object.freeze(
|
|
71
|
+
ACTION_EFFECT_CLASSES.filter((effectClass) => effectClass !== "READ_ONLY"),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// States whose semantics are security-relevant: entering them must be owned
|
|
75
|
+
// by a canonical core service, never by a generic caller-controlled surface.
|
|
76
|
+
export const ACTION_SECURITY_SENSITIVE_STATES = Object.freeze([
|
|
77
|
+
"AUTHORIZED",
|
|
78
|
+
"VERIFIED",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export const COMMIT_UNKNOWN_RESULT_CODES = Object.freeze([
|
|
82
|
+
"IDEMPOTENCY_PROTECTED",
|
|
83
|
+
"RECONCILED_COMMITTED",
|
|
84
|
+
"RECONCILED_NOT_COMMITTED",
|
|
85
|
+
"AMBIGUOUS",
|
|
86
|
+
]);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { proposeAction, transitionAction } from "./actions.js";
|
|
2
|
+
import { actionRequiresIdempotency } from "./action-model.js";
|
|
3
|
+
import { authorizeAction } from "./action-authorization.js";
|
|
4
|
+
import {
|
|
5
|
+
prepareCommandExecution,
|
|
6
|
+
runPreparedCommandExecution,
|
|
7
|
+
} from "./prepared-execution.js";
|
|
8
|
+
|
|
9
|
+
function actionExecutionError(code, message) {
|
|
10
|
+
const error = new Error(message);
|
|
11
|
+
error.code = code;
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function startedExecutionOutcome(action, execution) {
|
|
16
|
+
if (execution.status === "passed") return "COMMITTED";
|
|
17
|
+
if (!actionRequiresIdempotency(action.effectClass)) return "FAILED";
|
|
18
|
+
if (execution.termination === "spawn-error") return "FAILED";
|
|
19
|
+
return "COMMIT_UNKNOWN";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute a durable side-effecting action.
|
|
24
|
+
*
|
|
25
|
+
* Ordering is the security contract (INV-EXEC-01):
|
|
26
|
+
* propose -> prepare (deterministic pre-launch checks, no durable write)
|
|
27
|
+
* -> authorize (policy-bound AUTHORIZED)
|
|
28
|
+
* -> ACTION_STARTED (last protocol mutation before launch)
|
|
29
|
+
* -> exact-argv launch (INV-EXEC-02 conservative outcomes).
|
|
30
|
+
*/
|
|
31
|
+
export async function executeDurableAction({
|
|
32
|
+
target, packageRoot, taskId, input, argv, approvalId, authorityContext, runtimeContext, timeoutMs,
|
|
33
|
+
}) {
|
|
34
|
+
if (!Array.isArray(argv) || argv.length === 0) {
|
|
35
|
+
throw actionExecutionError("E_ACTION_INVALID", "run-action requires exact argv after --");
|
|
36
|
+
}
|
|
37
|
+
const proposed = await proposeAction(target, { packageRoot, taskId, input: {
|
|
38
|
+
...input,
|
|
39
|
+
provenance: "FORGELOOP_EXECUTED",
|
|
40
|
+
} });
|
|
41
|
+
const action = proposed.action;
|
|
42
|
+
if (action.state === "COMMIT_UNKNOWN") {
|
|
43
|
+
throw actionExecutionError("E_ACTION_RECONCILIATION_REQUIRED", `action ${action.actionId} must be reconciled before retry`);
|
|
44
|
+
}
|
|
45
|
+
if (action.state !== "PROPOSED") {
|
|
46
|
+
throw actionExecutionError("E_ACTION_STATE_MISMATCH", `action ${action.actionId} is already ${action.state}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Deterministic pre-launch checks. Any failure here leaves the action
|
|
50
|
+
// PROPOSED with no ACTION_STARTED event and no ambiguity about whether the
|
|
51
|
+
// external effect may have happened.
|
|
52
|
+
const prepared = await prepareCommandExecution({
|
|
53
|
+
target,
|
|
54
|
+
taskId,
|
|
55
|
+
argv,
|
|
56
|
+
details: { requirement: input.requirement ?? null },
|
|
57
|
+
authorityContext,
|
|
58
|
+
runtimeContext,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const { action: authorized, authorization } = await authorizeAction({
|
|
62
|
+
target, packageRoot, taskId, actionId: action.actionId,
|
|
63
|
+
approvalId, authorityContext,
|
|
64
|
+
});
|
|
65
|
+
const started = await transitionAction(target, {
|
|
66
|
+
packageRoot, taskId, actionId: action.actionId, to: "STARTED",
|
|
67
|
+
expectedRevision: authorized.revision, expectedFingerprint: action.actionFingerprint,
|
|
68
|
+
});
|
|
69
|
+
let execution;
|
|
70
|
+
try {
|
|
71
|
+
execution = await runPreparedCommandExecution({
|
|
72
|
+
target, packageRoot, taskId, checkId: `action:${action.actionId}`,
|
|
73
|
+
requirement: input.requirement ?? `durable action ${action.actionId}`,
|
|
74
|
+
prepared, timeoutMs,
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Persistence uncertainty after the launch boundary can never downgrade a
|
|
78
|
+
// side effect to FAILED: recover to COMMIT_UNKNOWN instead.
|
|
79
|
+
const to = actionRequiresIdempotency(action.effectClass) ? "COMMIT_UNKNOWN" : "FAILED";
|
|
80
|
+
await transitionAction(target, {
|
|
81
|
+
packageRoot, taskId, actionId: action.actionId, to,
|
|
82
|
+
expectedRevision: started.revision,
|
|
83
|
+
details: {
|
|
84
|
+
reason: to === "COMMIT_UNKNOWN"
|
|
85
|
+
? "execution outcome could not be proven after action start"
|
|
86
|
+
: "process did not launch",
|
|
87
|
+
...(to === "COMMIT_UNKNOWN" ? { commitResultCode: "AMBIGUOUS" } : {}),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const to = startedExecutionOutcome(action, execution.execution);
|
|
93
|
+
const result = await transitionAction(target, {
|
|
94
|
+
packageRoot, taskId, actionId: action.actionId, to,
|
|
95
|
+
expectedRevision: started.revision,
|
|
96
|
+
details: {
|
|
97
|
+
evidenceRef: execution.execution.executionId,
|
|
98
|
+
...(to === "COMMIT_UNKNOWN" ? {
|
|
99
|
+
commitResultCode: "AMBIGUOUS",
|
|
100
|
+
reason: `started execution ended via ${execution.execution.termination} without proving external commit state`,
|
|
101
|
+
} : {}),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return { action: result, execution: execution.execution, executionPath: execution.path, authorization };
|
|
105
|
+
}
|