codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -388
- package/dist/reviewer-runtime-build.json +89 -29
- package/dist/src/alpha6/adoption.d.ts +2 -0
- package/dist/src/alpha6/adoption.js +22 -0
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
- package/dist/src/alpha6/captured-check-evidence.js +152 -0
- package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +15 -1
- package/dist/src/alpha6/component-owner.js +44 -2
- package/dist/src/alpha6/component-owner.js.map +1 -1
- package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
- package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
- package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.d.ts +1 -25
- package/dist/src/alpha6/downstream-proof.js +0 -171
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
- package/dist/src/alpha6/literal-test-invocation.js +106 -0
- package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +2 -0
- package/dist/src/alpha6/milestone.js +10 -1
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +7 -5
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +6 -21
- package/dist/src/alpha6/remediation.js +161 -476
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/beta1/project-transaction.d.ts +3 -1
- package/dist/src/beta1/project-transaction.js +6 -2
- package/dist/src/beta1/project-transaction.js.map +1 -1
- package/dist/src/checks/runner.d.ts +26 -0
- package/dist/src/checks/runner.js +223 -0
- package/dist/src/checks/runner.js.map +1 -0
- package/dist/src/checks/task-sync.d.ts +10 -0
- package/dist/src/checks/task-sync.js +41 -0
- package/dist/src/checks/task-sync.js.map +1 -0
- package/dist/src/checks/worker.d.ts +1 -0
- package/dist/src/checks/worker.js +154 -0
- package/dist/src/checks/worker.js.map +1 -0
- package/dist/src/cli-actions.d.ts +2 -2
- package/dist/src/cli-actions.js +3 -15
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +14 -98
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +35 -32
- package/dist/src/dependency-provenance.d.ts +2 -2
- package/dist/src/dependency-provenance.js +12 -69
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/base-sync-conflict.d.ts +13 -0
- package/dist/src/domain/base-sync-conflict.js +32 -0
- package/dist/src/domain/base-sync-conflict.js.map +1 -0
- package/dist/src/domain/step-start-admission.d.ts +4 -0
- package/dist/src/domain/step-start-admission.js +9 -0
- package/dist/src/domain/step-start-admission.js.map +1 -0
- package/dist/src/domain/virgin-registration.d.ts +3 -0
- package/dist/src/domain/virgin-registration.js +65 -0
- package/dist/src/domain/virgin-registration.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/gateway-handshake.js +0 -8
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/git.d.ts +3 -1
- package/dist/src/git.js +41 -14
- package/dist/src/git.js.map +1 -1
- package/dist/src/graph.js +25 -3
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/navigation-actions.d.ts +9 -0
- package/dist/src/navigation-actions.js +72 -0
- package/dist/src/navigation-actions.js.map +1 -0
- package/dist/src/navigation-update-artifact.d.ts +5 -0
- package/dist/src/navigation-update-artifact.js +227 -0
- package/dist/src/navigation-update-artifact.js.map +1 -0
- package/dist/src/navigation-update.d.ts +39 -0
- package/dist/src/navigation-update.js +82 -0
- package/dist/src/navigation-update.js.map +1 -0
- package/dist/src/observation.js +52 -29
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.js +5 -6
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +0 -13
- package/dist/src/pending-review-update.js +1 -6
- package/dist/src/pending-review-update.js.map +1 -1
- package/dist/src/reviewer.d.ts +1 -1
- package/dist/src/reviewer.js +20 -17
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.d.ts +10 -0
- package/dist/src/state/corrective-replan-executor.js +37 -1
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public-schema.js +15 -2
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +1 -1
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
- package/dist/src/state/corrective-replan-transaction.js +12 -11
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
- package/dist/src/state/corrective-yield-transaction.js +11 -10
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +4 -0
- package/dist/src/state/lock.js +21 -0
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow-blocker-route.d.ts +8 -0
- package/dist/src/workflow-blocker-route.js +106 -0
- package/dist/src/workflow-blocker-route.js.map +1 -0
- package/dist/src/workflow.d.ts +48 -63
- package/dist/src/workflow.js +666 -1460
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +26 -304
- package/docs/decisions.md +14 -112
- package/docs/development-flow.md +35 -238
- package/docs/project-memory.md +31 -50
- package/docs/release-app-evidence.md +190 -0
- package/docs/release.md +95 -388
- package/docs/updating-existing-project.md +19 -717
- package/package.json +11 -13
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
- package/references/state-machine.md +4 -4
- package/roles/technical-planner.md +1 -1
- package/schemas/corrective-decision-event.schema.json +3 -1
- package/schemas/project-knowledge-map.schema.json +3 -1
- package/schemas/remediation-event.schema.json +10 -1
- package/schemas/task.schema.json +46 -2
- package/schemas/transition-payloads.schema.json +16 -1
- package/src/alpha6/adoption.ts +1123 -0
- package/src/alpha6/captured-check-evidence.ts +132 -0
- package/src/alpha6/check-support-anchor.ts +128 -0
- package/src/alpha6/component-owner.ts +168 -0
- package/src/alpha6/corrective-decision-boundary.ts +39 -0
- package/src/alpha6/downstream-proof.ts +520 -0
- package/src/alpha6/failed-step-planning-recovery.ts +88 -0
- package/src/alpha6/handoff.ts +1443 -0
- package/src/alpha6/journal.ts +473 -0
- package/src/alpha6/literal-test-invocation.ts +84 -0
- package/src/alpha6/mechanical-feasibility.ts +488 -0
- package/src/alpha6/milestone.ts +2192 -0
- package/src/alpha6/navigation-dirty-carryover.ts +154 -0
- package/src/alpha6/npm-check-contract.ts +47 -0
- package/src/alpha6/plan-integrity.ts +298 -0
- package/src/alpha6/plan-risk.ts +1480 -0
- package/src/alpha6/preexecution-replan.ts +187 -0
- package/src/alpha6/remediation-cause.ts +98 -0
- package/src/alpha6/remediation.ts +2438 -0
- package/src/alpha6/review.ts +1198 -0
- package/src/alpha6/root-cause-replan-carryover.ts +491 -0
- package/src/alpha6/store-sidecars.ts +335 -0
- package/src/alpha7/autonomy.ts +411 -0
- package/src/alpha7/corrective-recovery.ts +1332 -0
- package/src/artifacts.ts +130 -0
- package/src/beta1/project-transaction.ts +355 -0
- package/src/change-explanation.ts +153 -0
- package/src/checks/runner.ts +245 -0
- package/src/checks/task-sync.ts +41 -0
- package/src/checks/worker.ts +149 -0
- package/src/cli-actions.ts +107 -0
- package/src/cli.ts +1457 -0
- package/src/contracts.ts +1494 -0
- package/src/credential-output.ts +89 -0
- package/src/credential-transport.ts +215 -0
- package/src/delegation.ts +190 -0
- package/src/dependency-provenance.ts +472 -0
- package/src/diagnostics.ts +93 -0
- package/src/domain/base-sync-conflict.ts +35 -0
- package/src/domain/completed-step-carryover.ts +76 -0
- package/src/domain/discovery.ts +27 -0
- package/src/domain/plan-semantics.ts +58 -0
- package/src/domain/step-start-admission.ts +10 -0
- package/src/domain/validation.ts +177 -0
- package/src/domain/virgin-registration.ts +46 -0
- package/src/errors.ts +24 -0
- package/src/fs-utils.ts +61 -0
- package/src/gateway-handshake.ts +95 -0
- package/src/git.ts +183 -0
- package/src/graph.ts +342 -0
- package/src/historical-step-provenance.ts +136 -0
- package/src/index.ts +23 -0
- package/src/lifecycle/canonical-hash.ts +28 -0
- package/src/lifecycle/catalog.ts +202 -0
- package/src/lifecycle/compiler-inspection.ts +29 -0
- package/src/lifecycle/core-static-readiness.ts +132 -0
- package/src/lifecycle/corrective-replan-authority.ts +136 -0
- package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
- package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
- package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
- package/src/lifecycle/corrective-replan-credentials.ts +48 -0
- package/src/lifecycle/corrective-replan.ts +843 -0
- package/src/lifecycle/evaluator.ts +48 -0
- package/src/lifecycle/fingerprint.ts +488 -0
- package/src/lifecycle/immutable.ts +8 -0
- package/src/lifecycle/implementation-table.ts +118 -0
- package/src/lifecycle/index.ts +8 -0
- package/src/lifecycle/schema-artifact.ts +263 -0
- package/src/lifecycle/semantic-registry.ts +572 -0
- package/src/lifecycle/types.ts +838 -0
- package/src/memory.ts +273 -0
- package/src/migration.ts +161 -0
- package/src/navigation-actions.ts +70 -0
- package/src/navigation-update-artifact.ts +198 -0
- package/src/navigation-update.ts +121 -0
- package/src/observation.ts +225 -0
- package/src/observed-routes.ts +660 -0
- package/src/operational-contract.ts +125 -0
- package/src/pending-review-update.ts +175 -0
- package/src/repository.ts +99 -0
- package/src/reviewer.ts +1879 -0
- package/src/runtime.ts +6 -0
- package/src/state/corrective-replan-executor.ts +818 -0
- package/src/state/corrective-replan-public-schema.ts +83 -0
- package/src/state/corrective-replan-public.ts +908 -0
- package/src/state/corrective-replan-transaction.ts +949 -0
- package/src/state/corrective-yield-executor.ts +327 -0
- package/src/state/corrective-yield-transaction.ts +730 -0
- package/src/state/lock.ts +902 -0
- package/src/state/store.ts +567 -0
- package/src/transition-core.ts +330 -0
- package/src/ulid.ts +24 -0
- package/src/version.ts +2 -0
- package/src/workflow-blocker-route.ts +109 -0
- package/src/workflow.ts +10172 -0
- package/docs/alpha7.1-implementation-brief.md +0 -268
- package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
- package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
- package/docs/beta1-stabilization-brief.md +0 -165
- package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
- package/docs/beta13.2-signal-review-recovery.md +0 -38
- package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
- package/docs/change-model.md +0 -118
- package/docs/delegated-approval.md +0 -254
- package/docs/lifecycle/state-machine-stabilization.md +0 -641
- package/docs/pdf/README.md +0 -24
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +0 -1
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
- package/docs/pending-review-update.md +0 -15
- package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
- package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
- package/docs/problem-briefs/README.md +0 -120
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
- package/docs/split-required-recovery.md +0 -47
- package/docs/stable-release-defect-register.md +0 -730
- package/docs/validation-report.md +0 -182
- package/scripts/generate-pdf-docs.py +0 -524
- package/scripts/run-pdf-docs.mjs +0 -62
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const CREDENTIAL_FIELD_NAMES = new Set(['token', 'writerToken', 'claimToken']);
|
|
2
|
+
|
|
3
|
+
export interface CredentialOutputAllowance {
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class UnsafeCredentialOutputError extends Error {
|
|
8
|
+
readonly paths: string[];
|
|
9
|
+
|
|
10
|
+
constructor(paths: string[]) {
|
|
11
|
+
super('CLI output contained credentials outside an exact issuance receipt allowance.');
|
|
12
|
+
this.name = 'UnsafeCredentialOutputError';
|
|
13
|
+
this.paths = [...new Set(paths)].sort();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function assertCliSuccessCredentialSafety(
|
|
18
|
+
value: unknown,
|
|
19
|
+
allowances: readonly CredentialOutputAllowance[],
|
|
20
|
+
sensitiveInputs: readonly string[],
|
|
21
|
+
): void {
|
|
22
|
+
const allowedPaths = new Set(allowances.map((allowance) => allowance.path));
|
|
23
|
+
const secrets = new Set(sensitiveInputs.filter(Boolean));
|
|
24
|
+
const violations: string[] = [];
|
|
25
|
+
visit(value, '$', (path, key, candidate) => {
|
|
26
|
+
const credentialField = key !== null
|
|
27
|
+
&& CREDENTIAL_FIELD_NAMES.has(key)
|
|
28
|
+
&& candidate !== null
|
|
29
|
+
&& candidate !== undefined;
|
|
30
|
+
const containingSecrets = typeof candidate === 'string'
|
|
31
|
+
? [...secrets].filter((secret) => candidate.includes(secret))
|
|
32
|
+
: [];
|
|
33
|
+
const exactAllowedIssuanceValue = credentialField
|
|
34
|
+
&& allowedPaths.has(path)
|
|
35
|
+
&& typeof candidate === 'string'
|
|
36
|
+
&& containingSecrets.some((secret) => candidate === secret);
|
|
37
|
+
if (credentialField && !allowedPaths.has(path)) violations.push(path);
|
|
38
|
+
if (containingSecrets.length > 0 && !exactAllowedIssuanceValue) violations.push(path);
|
|
39
|
+
});
|
|
40
|
+
if (violations.length > 0) throw new UnsafeCredentialOutputError(violations);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function projectCliErrorValue(value: unknown, sensitiveInputs: readonly string[]): unknown {
|
|
44
|
+
const secrets = new Set(sensitiveInputs.filter(Boolean));
|
|
45
|
+
return project(value, secrets);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function projectCliErrorMessage(message: string, sensitiveInputs: readonly string[]): string {
|
|
49
|
+
return sensitiveInputs
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.sort((left, right) => right.length - left.length)
|
|
52
|
+
.reduce((projected, secret) => projected.replaceAll(secret, '[REDACTED_CREDENTIAL]'), message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function project(value: unknown, secrets: ReadonlySet<string>): unknown {
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
return [...secrets]
|
|
58
|
+
.sort((left, right) => right.length - left.length)
|
|
59
|
+
.reduce((projected, secret) => projected.replaceAll(secret, '[REDACTED_CREDENTIAL]'), value);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) return value.map((entry) => project(entry, secrets));
|
|
62
|
+
if (!isRecord(value)) return value;
|
|
63
|
+
return Object.fromEntries(
|
|
64
|
+
Object.entries(value)
|
|
65
|
+
.filter(([key]) => !CREDENTIAL_FIELD_NAMES.has(key))
|
|
66
|
+
.map(([key, candidate]) => [key, project(candidate, secrets)]),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function visit(
|
|
71
|
+
value: unknown,
|
|
72
|
+
path: string,
|
|
73
|
+
visitor: (path: string, key: string | null, value: unknown) => void,
|
|
74
|
+
key: string | null = null,
|
|
75
|
+
): void {
|
|
76
|
+
visitor(path, key, value);
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
value.forEach((entry, index) => visit(entry, `${path}[${index}]`, visitor));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!isRecord(value)) return;
|
|
82
|
+
for (const [childKey, child] of Object.entries(value)) {
|
|
83
|
+
visit(child, `${path}.${childKey}`, visitor, childKey);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
88
|
+
return typeof value === 'object' && value !== null;
|
|
89
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { WorkflowError } from './errors.js';
|
|
11
|
+
import { ensureDirectory, writeJsonAtomic } from './fs-utils.js';
|
|
12
|
+
|
|
13
|
+
export type CredentialKind = 'writer-lease' | 'claim-token';
|
|
14
|
+
|
|
15
|
+
interface StoredCredentialReference {
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
referenceId: string;
|
|
18
|
+
kind: CredentialKind;
|
|
19
|
+
projectId: string;
|
|
20
|
+
entityId: string;
|
|
21
|
+
actor: string;
|
|
22
|
+
allowedActions: string[];
|
|
23
|
+
token: string;
|
|
24
|
+
tokenSha256: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
expiresAt: string;
|
|
27
|
+
oneTime: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PublicCredentialReference {
|
|
31
|
+
credentialRef: string;
|
|
32
|
+
kind: CredentialKind;
|
|
33
|
+
entityId: string;
|
|
34
|
+
actor: string;
|
|
35
|
+
allowedActions: string[];
|
|
36
|
+
expiresAt: string;
|
|
37
|
+
transport: 'local-vault-v1';
|
|
38
|
+
oneTime: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const REFERENCE_PATTERN = /^CREF-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/;
|
|
42
|
+
|
|
43
|
+
export function issueCredentialReference(input: {
|
|
44
|
+
stateRoot: string;
|
|
45
|
+
projectId: string;
|
|
46
|
+
entityId: string;
|
|
47
|
+
actor: string;
|
|
48
|
+
kind: CredentialKind;
|
|
49
|
+
token: string;
|
|
50
|
+
allowedActions: readonly string[];
|
|
51
|
+
expiresAt: string;
|
|
52
|
+
oneTime: boolean;
|
|
53
|
+
now?: Date;
|
|
54
|
+
}): PublicCredentialReference {
|
|
55
|
+
const token = input.token.trim();
|
|
56
|
+
if (!token) throw new WorkflowError('STATE_CORRUPT', 'Cannot issue an empty credential reference.');
|
|
57
|
+
const referenceId = `CREF-${randomUUID()}`;
|
|
58
|
+
const actor = input.actor.trim();
|
|
59
|
+
const allowedActions = [...new Set(input.allowedActions.map((value) => value.trim()).filter(Boolean))].sort();
|
|
60
|
+
if (!actor || allowedActions.length === 0) {
|
|
61
|
+
throw new WorkflowError('STATE_CORRUPT', 'Credential reference requires an actor and at least one action.');
|
|
62
|
+
}
|
|
63
|
+
const record: StoredCredentialReference = {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
referenceId,
|
|
66
|
+
kind: input.kind,
|
|
67
|
+
projectId: input.projectId,
|
|
68
|
+
entityId: input.entityId,
|
|
69
|
+
actor,
|
|
70
|
+
allowedActions,
|
|
71
|
+
token,
|
|
72
|
+
tokenSha256: createHash('sha256').update(token).digest('hex'),
|
|
73
|
+
createdAt: (input.now ?? new Date()).toISOString(),
|
|
74
|
+
expiresAt: new Date(input.expiresAt).toISOString(),
|
|
75
|
+
oneTime: input.oneTime,
|
|
76
|
+
};
|
|
77
|
+
const file = credentialFile(input.stateRoot, input.projectId, referenceId);
|
|
78
|
+
writeJsonAtomic(file, record);
|
|
79
|
+
chmodSync(file, 0o600);
|
|
80
|
+
return publicReference(record);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveCredentialReference(input: {
|
|
84
|
+
stateRoot: string;
|
|
85
|
+
projectId: string;
|
|
86
|
+
reference: string;
|
|
87
|
+
kind: CredentialKind;
|
|
88
|
+
entityId: string;
|
|
89
|
+
actor?: string | null;
|
|
90
|
+
action: string;
|
|
91
|
+
now?: Date;
|
|
92
|
+
}): { token: string; reference: PublicCredentialReference } {
|
|
93
|
+
const file = credentialFile(input.stateRoot, input.projectId, input.reference);
|
|
94
|
+
if (!existsSync(file)) {
|
|
95
|
+
throw new WorkflowError('NOT_FOUND', 'Local credential reference is missing or already consumed.', {
|
|
96
|
+
credentialRef: input.reference,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const metadata = lstatSync(file);
|
|
100
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || (metadata.mode & 0o077) !== 0) {
|
|
101
|
+
throw new WorkflowError('STATE_CORRUPT', 'Local credential reference must be a private regular file.', {
|
|
102
|
+
credentialRef: input.reference,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
let value: unknown;
|
|
106
|
+
try {
|
|
107
|
+
value = JSON.parse(readFileSync(file, 'utf8')) as unknown;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new WorkflowError('STATE_CORRUPT', 'Local credential reference is not valid JSON.', {
|
|
110
|
+
credentialRef: input.reference,
|
|
111
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const record = validateRecord(value, input.reference);
|
|
115
|
+
const actor = input.actor?.trim() || record.actor;
|
|
116
|
+
const now = (input.now ?? new Date()).getTime();
|
|
117
|
+
if (
|
|
118
|
+
record.projectId !== input.projectId
|
|
119
|
+
|| record.kind !== input.kind
|
|
120
|
+
|| record.entityId !== input.entityId
|
|
121
|
+
|| record.actor !== actor
|
|
122
|
+
|| !record.allowedActions.includes(input.action)
|
|
123
|
+
) {
|
|
124
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Local credential reference is not bound to this transition.', {
|
|
125
|
+
credentialRef: input.reference,
|
|
126
|
+
expected: { kind: input.kind, entityId: input.entityId, actor, action: input.action },
|
|
127
|
+
actual: {
|
|
128
|
+
kind: record.kind,
|
|
129
|
+
entityId: record.entityId,
|
|
130
|
+
actor: record.actor,
|
|
131
|
+
allowedActions: record.allowedActions,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (Date.parse(record.expiresAt) <= now) {
|
|
136
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Local credential reference has expired.', {
|
|
137
|
+
credentialRef: input.reference,
|
|
138
|
+
expiresAt: record.expiresAt,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (createHash('sha256').update(record.token).digest('hex') !== record.tokenSha256) {
|
|
142
|
+
throw new WorkflowError('STATE_CORRUPT', 'Local credential reference failed its integrity check.', {
|
|
143
|
+
credentialRef: input.reference,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return { token: record.token, reference: publicReference(record) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function consumeCredentialReference(stateRoot: string, projectId: string, reference: string): void {
|
|
150
|
+
const file = credentialFile(stateRoot, projectId, reference);
|
|
151
|
+
if (!existsSync(file)) return;
|
|
152
|
+
const metadata = lstatSync(file);
|
|
153
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
154
|
+
throw new WorkflowError('STATE_CORRUPT', 'Refusing to consume a non-regular credential reference.', {
|
|
155
|
+
credentialRef: reference,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
unlinkSync(file);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function assertNoLegacyCredentialArguments(optionNames: ReadonlySet<string>): void {
|
|
162
|
+
const legacy = ['writer-token', 'claim-token'].filter((name) => optionNames.has(name));
|
|
163
|
+
if (legacy.length === 0) return;
|
|
164
|
+
throw new WorkflowError(
|
|
165
|
+
'INVALID_ARGUMENT',
|
|
166
|
+
'Secret-bearing CLI options are disabled. Use --writer-credential-ref or --claim-credential-ref.',
|
|
167
|
+
{ rejectedOptions: legacy.map((name) => `--${name}`), credentialTransport: 'local-vault-v1' },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function credentialFile(stateRoot: string, projectId: string, reference: string): string {
|
|
172
|
+
const match = REFERENCE_PATTERN.exec(reference);
|
|
173
|
+
if (!match) throw new WorkflowError('INVALID_ARGUMENT', 'Credential reference has an invalid format.');
|
|
174
|
+
if (!/^[a-f0-9]{24}$/.test(projectId)) throw new WorkflowError('STATE_CORRUPT', 'Credential project identity is invalid.');
|
|
175
|
+
const root = path.join(path.resolve(stateRoot), 'credentials', projectId);
|
|
176
|
+
ensureDirectory(root);
|
|
177
|
+
const file = path.join(root, `${reference}.json`);
|
|
178
|
+
if (path.dirname(file) !== root) throw new WorkflowError('STATE_CORRUPT', 'Credential reference escaped its vault.');
|
|
179
|
+
return file;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateRecord(value: unknown, reference: string): StoredCredentialReference {
|
|
183
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
184
|
+
throw new WorkflowError('STATE_CORRUPT', 'Credential reference record is invalid.', { credentialRef: reference });
|
|
185
|
+
}
|
|
186
|
+
const record = value as Partial<StoredCredentialReference>;
|
|
187
|
+
const valid = record.schemaVersion === 1
|
|
188
|
+
&& record.referenceId === reference
|
|
189
|
+
&& (record.kind === 'writer-lease' || record.kind === 'claim-token')
|
|
190
|
+
&& typeof record.projectId === 'string'
|
|
191
|
+
&& typeof record.entityId === 'string'
|
|
192
|
+
&& typeof record.actor === 'string'
|
|
193
|
+
&& Array.isArray(record.allowedActions)
|
|
194
|
+
&& record.allowedActions.every((entry) => typeof entry === 'string')
|
|
195
|
+
&& typeof record.token === 'string'
|
|
196
|
+
&& typeof record.tokenSha256 === 'string'
|
|
197
|
+
&& typeof record.createdAt === 'string'
|
|
198
|
+
&& typeof record.expiresAt === 'string'
|
|
199
|
+
&& typeof record.oneTime === 'boolean';
|
|
200
|
+
if (!valid) throw new WorkflowError('STATE_CORRUPT', 'Credential reference record has an invalid schema.', { credentialRef: reference });
|
|
201
|
+
return record as StoredCredentialReference;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function publicReference(record: StoredCredentialReference): PublicCredentialReference {
|
|
205
|
+
return {
|
|
206
|
+
credentialRef: record.referenceId,
|
|
207
|
+
kind: record.kind,
|
|
208
|
+
entityId: record.entityId,
|
|
209
|
+
actor: record.actor,
|
|
210
|
+
allowedActions: [...record.allowedActions],
|
|
211
|
+
expiresAt: record.expiresAt,
|
|
212
|
+
transport: 'local-vault-v1',
|
|
213
|
+
oneTime: record.oneTime,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
DelegatedAuthorizationEvidence,
|
|
4
|
+
DelegationGrant,
|
|
5
|
+
DelegationGrantGate,
|
|
6
|
+
DelegationPolicyInput,
|
|
7
|
+
DelegationTransition,
|
|
8
|
+
MilestoneState,
|
|
9
|
+
ProjectKnowledgeMap,
|
|
10
|
+
TaskState,
|
|
11
|
+
} from './contracts.js';
|
|
12
|
+
import { WorkflowError } from './errors.js';
|
|
13
|
+
|
|
14
|
+
const TRANSITIONS: readonly DelegationTransition[] = [
|
|
15
|
+
'project_memory.approve',
|
|
16
|
+
'task.execution_authorize',
|
|
17
|
+
'task.final_accept',
|
|
18
|
+
'milestone.execution_authorize',
|
|
19
|
+
'milestone.final_accept',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
type DelegationTarget = TaskState | MilestoneState | ProjectKnowledgeMap;
|
|
23
|
+
|
|
24
|
+
export function prepareDelegationGrantGate(
|
|
25
|
+
projectId: string,
|
|
26
|
+
input: DelegationPolicyInput,
|
|
27
|
+
now: Date,
|
|
28
|
+
): DelegationGrantGate {
|
|
29
|
+
const policy = normalizeDelegationPolicy(input, now);
|
|
30
|
+
const policyHash = hashDelegationPolicy(projectId, policy);
|
|
31
|
+
const binding = ['delegation-grant-issuance-v1', projectId, policyHash].join('\0');
|
|
32
|
+
return {
|
|
33
|
+
kind: 'delegation_grant_issuance',
|
|
34
|
+
projectId,
|
|
35
|
+
policy,
|
|
36
|
+
policyHash,
|
|
37
|
+
confirmationCode: `DGA-${createHash('sha256').update(binding).digest('hex').slice(0, 24).toUpperCase()}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeDelegationPolicy(
|
|
42
|
+
input: DelegationPolicyInput,
|
|
43
|
+
now: Date,
|
|
44
|
+
): DelegationPolicyInput {
|
|
45
|
+
const principal = input.principal?.trim();
|
|
46
|
+
const delegate = input.delegate?.trim();
|
|
47
|
+
if (!principal || !delegate) {
|
|
48
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation requires non-empty principal and delegate values.');
|
|
49
|
+
}
|
|
50
|
+
if (principal === delegate) {
|
|
51
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation principal and delegate must be distinct audit actors.');
|
|
52
|
+
}
|
|
53
|
+
if (!input.scope || !['project', 'milestone', 'task'].includes(input.scope.kind)) {
|
|
54
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation scope must be project, milestone, or task.');
|
|
55
|
+
}
|
|
56
|
+
const scope = normalizeScope(input.scope);
|
|
57
|
+
if (!Array.isArray(input.transitions) || input.transitions.length === 0) {
|
|
58
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation requires at least one allowed transition.');
|
|
59
|
+
}
|
|
60
|
+
const transitions = [...new Set(input.transitions)];
|
|
61
|
+
if (transitions.some((transition) => !TRANSITIONS.includes(transition))) {
|
|
62
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation contains an unsupported transition.', {
|
|
63
|
+
supportedTransitions: TRANSITIONS,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
transitions.sort();
|
|
67
|
+
if (transitions.includes('project_memory.approve') && scope.kind !== 'project') {
|
|
68
|
+
throw new WorkflowError(
|
|
69
|
+
'INVALID_ARGUMENT',
|
|
70
|
+
'project_memory.approve requires project scope.',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const expiresAtDate = new Date(input.expiresAt);
|
|
74
|
+
if (!Number.isFinite(expiresAtDate.getTime())) {
|
|
75
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation expiresAt must be a valid ISO-8601 timestamp.');
|
|
76
|
+
}
|
|
77
|
+
if (expiresAtDate.getTime() <= now.getTime()) {
|
|
78
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation expiresAt must be in the future.');
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
principal,
|
|
82
|
+
delegate,
|
|
83
|
+
scope,
|
|
84
|
+
transitions,
|
|
85
|
+
expiresAt: expiresAtDate.toISOString(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function hashDelegationPolicy(projectId: string, policy: DelegationPolicyInput): string {
|
|
90
|
+
return createHash('sha256')
|
|
91
|
+
.update(JSON.stringify({
|
|
92
|
+
version: 'delegation-policy-v1',
|
|
93
|
+
projectId,
|
|
94
|
+
principal: policy.principal,
|
|
95
|
+
delegate: policy.delegate,
|
|
96
|
+
scope: policy.scope,
|
|
97
|
+
transitions: policy.transitions,
|
|
98
|
+
expiresAt: policy.expiresAt,
|
|
99
|
+
}))
|
|
100
|
+
.digest('hex');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function assertDelegationCanAuthorize(
|
|
104
|
+
grant: DelegationGrant,
|
|
105
|
+
transition: DelegationTransition,
|
|
106
|
+
actor: string,
|
|
107
|
+
target: DelegationTarget,
|
|
108
|
+
now: Date,
|
|
109
|
+
): DelegatedAuthorizationEvidence {
|
|
110
|
+
const expectedPolicyHash = hashDelegationPolicy(grant.projectId, {
|
|
111
|
+
principal: grant.principal,
|
|
112
|
+
delegate: grant.delegate,
|
|
113
|
+
scope: grant.scope,
|
|
114
|
+
transitions: grant.transitions,
|
|
115
|
+
expiresAt: grant.expiresAt,
|
|
116
|
+
});
|
|
117
|
+
if (grant.policyHash !== expectedPolicyHash) {
|
|
118
|
+
throw new WorkflowError('STATE_CORRUPT', `Delegation ${grant.id} policy hash is invalid.`, {
|
|
119
|
+
expectedPolicyHash,
|
|
120
|
+
actualPolicyHash: grant.policyHash,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const expiry = new Date(grant.expiresAt).getTime();
|
|
124
|
+
if (!Number.isFinite(expiry)) {
|
|
125
|
+
throw new WorkflowError('STATE_CORRUPT', `Delegation ${grant.id} has an invalid expiresAt value.`);
|
|
126
|
+
}
|
|
127
|
+
if (grant.status !== 'active') {
|
|
128
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} is ${grant.status}.`);
|
|
129
|
+
}
|
|
130
|
+
if (expiry <= now.getTime()) {
|
|
131
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} has expired.`, {
|
|
132
|
+
expiresAt: grant.expiresAt,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const normalizedActor = actor.trim();
|
|
136
|
+
if (!normalizedActor || normalizedActor !== grant.delegate) {
|
|
137
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} does not authorize actor ${normalizedActor || '<empty>'}.`, {
|
|
138
|
+
expectedDelegate: grant.delegate,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!grant.transitions.includes(transition)) {
|
|
142
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} does not allow ${transition}.`);
|
|
143
|
+
}
|
|
144
|
+
if (!delegationScopeMatches(grant, target)) {
|
|
145
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} does not cover ${target.id}.`, {
|
|
146
|
+
scope: grant.scope,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
grantId: grant.id,
|
|
151
|
+
policyHash: grant.policyHash,
|
|
152
|
+
principal: grant.principal,
|
|
153
|
+
delegate: grant.delegate,
|
|
154
|
+
transition,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function delegationCanAuthorize(
|
|
159
|
+
grant: DelegationGrant,
|
|
160
|
+
transition: DelegationTransition,
|
|
161
|
+
target: DelegationTarget,
|
|
162
|
+
now: Date,
|
|
163
|
+
): boolean {
|
|
164
|
+
const expiry = new Date(grant.expiresAt).getTime();
|
|
165
|
+
return grant.status === 'active'
|
|
166
|
+
&& Number.isFinite(expiry)
|
|
167
|
+
&& expiry > now.getTime()
|
|
168
|
+
&& grant.transitions.includes(transition)
|
|
169
|
+
&& delegationScopeMatches(grant, target);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function delegationScopeMatches(grant: DelegationGrant, target: DelegationTarget): boolean {
|
|
173
|
+
if (grant.scope.kind === 'project') return true;
|
|
174
|
+
if (target.kind === 'knowledge-map') return false;
|
|
175
|
+
if (grant.scope.kind === 'task') return target.kind === 'task' && grant.scope.id === target.id;
|
|
176
|
+
if (target.kind === 'milestone') return grant.scope.id === target.id;
|
|
177
|
+
return target.milestoneId === grant.scope.id;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeScope(scope: DelegationPolicyInput['scope']): DelegationPolicyInput['scope'] {
|
|
181
|
+
if (scope.kind === 'project') return { kind: 'project' };
|
|
182
|
+
const id = scope.id?.trim();
|
|
183
|
+
const pattern = scope.kind === 'milestone'
|
|
184
|
+
? /^MS-[0-9A-HJKMNP-TV-Z]{26}$/
|
|
185
|
+
: /^TASK-[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
186
|
+
if (!id || !pattern.test(id)) {
|
|
187
|
+
throw new WorkflowError('INVALID_ARGUMENT', `Delegation ${scope.kind} scope requires a valid entity ID.`);
|
|
188
|
+
}
|
|
189
|
+
return { kind: scope.kind, id };
|
|
190
|
+
}
|