@yunhozz/arc 0.1.7 → 0.1.10
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/MANIFEST.sha256 +86 -61
- package/README.md +1 -1
- package/claude/agents/{arc-coder.md → arc-implementer.md} +7 -7
- package/claude/agents/arc-planner.md +4 -4
- package/claude/agents/arc-qa.md +4 -4
- package/claude/agents/arc-reviewer.md +5 -5
- package/claude/agents/arc-scouter.md +3 -3
- package/claude/skill/SKILL.md +7 -7
- package/claude/skill/models.toml +3 -3
- package/claude/skill/references/commit.md +22 -7
- package/claude/skill/references/coordination.md +25 -18
- package/claude/skill/references/git-recovery.md +1 -1
- package/claude/skill/references/handoff-prep.md +25 -2
- package/claude/skill/references/handoff.md +106 -24
- package/claude/skill/references/planning.md +73 -22
- package/claude/skill/references/verification.md +155 -17
- package/claude/skill/runtime/acceptance.mjs +68 -0
- package/claude/skill/runtime/assignment.mjs +42 -0
- package/claude/skill/runtime/bootstrap.mjs +35 -0
- package/claude/skill/runtime/cooperative-schema.mjs +55 -0
- package/claude/skill/runtime/cooperative.mjs +136 -0
- package/claude/skill/runtime/evaluate.mjs +40 -4
- package/claude/skill/runtime/external.mjs +51 -0
- package/claude/skill/runtime/gate.mjs +7 -7
- package/claude/skill/runtime/git-evidence.mjs +46 -0
- package/claude/skill/runtime/operations.mjs +2 -2
- package/claude/skill/runtime/role-policy.json +64 -19
- package/claude/skill/runtime/schema.mjs +1 -0
- package/claude/skill/runtime/scope.mjs +36 -0
- package/claude/skill/runtime/store.mjs +25 -4
- package/claude/skill/runtime/verification.mjs +123 -0
- package/claude/skill/scripts/model_config.py +2 -2
- package/codex/agents/{arc-coder.toml → arc-implementer.toml} +8 -8
- package/codex/agents/arc-planner.toml +6 -6
- package/codex/agents/arc-qa.toml +6 -6
- package/codex/agents/arc-reviewer.toml +6 -6
- package/codex/agents/arc-scouter.toml +5 -5
- package/codex/skill/SKILL.md +9 -9
- package/codex/skill/models.toml +3 -3
- package/codex/skill/references/commit.md +23 -8
- package/codex/skill/references/coordination.md +25 -18
- package/codex/skill/references/git-recovery.md +2 -2
- package/codex/skill/references/handoff-prep.md +25 -2
- package/codex/skill/references/handoff-worktree.md +4 -4
- package/codex/skill/references/handoff.md +116 -34
- package/codex/skill/references/planning.md +76 -23
- package/codex/skill/references/verification.md +160 -22
- package/codex/skill/runtime/acceptance.mjs +68 -0
- package/codex/skill/runtime/assignment.mjs +42 -0
- package/codex/skill/runtime/bootstrap.mjs +35 -0
- package/codex/skill/runtime/cooperative-schema.mjs +55 -0
- package/codex/skill/runtime/cooperative.mjs +136 -0
- package/codex/skill/runtime/evaluate.mjs +40 -4
- package/codex/skill/runtime/external.mjs +51 -0
- package/codex/skill/runtime/gate.mjs +7 -7
- package/codex/skill/runtime/git-evidence.mjs +46 -0
- package/codex/skill/runtime/operations.mjs +2 -2
- package/codex/skill/runtime/role-policy.json +64 -19
- package/codex/skill/runtime/schema.mjs +1 -0
- package/codex/skill/runtime/scope.mjs +36 -0
- package/codex/skill/runtime/store.mjs +25 -4
- package/codex/skill/runtime/verification.mjs +123 -0
- package/codex/skill/scripts/model_config.py +3 -3
- package/lib/cli/root.mjs +1 -0
- package/lib/cli/workflow.mjs +21 -0
- package/lib/installer/manifest.mjs +1 -1
- package/lib/installer/plan.mjs +27 -13
- package/lib/installer/targets.mjs +1 -1
- package/lib/models/config.mjs +1 -1
- package/lib/models/profiles.mjs +1 -1
- package/lib/policy/placement.mjs +6 -6
- package/lib/policy/render.mjs +3 -3
- package/lib/policy/role-policy.json +64 -19
- package/lib/release/artifact.mjs +1 -1
- package/lib/runtime/acceptance.mjs +68 -0
- package/lib/runtime/assignment.mjs +42 -0
- package/lib/runtime/bootstrap.mjs +35 -0
- package/lib/runtime/cooperative-schema.mjs +55 -0
- package/lib/runtime/cooperative.mjs +136 -0
- package/lib/runtime/evaluate.mjs +40 -4
- package/lib/runtime/external.mjs +51 -0
- package/lib/runtime/gate.mjs +7 -7
- package/lib/runtime/git-evidence.mjs +46 -0
- package/lib/runtime/operations.mjs +2 -2
- package/lib/runtime/schema.mjs +1 -0
- package/lib/runtime/scope.mjs +36 -0
- package/lib/runtime/store.mjs +25 -4
- package/lib/runtime/verification.mjs +123 -0
- package/package.json +4 -2
- package/claude/skill/scripts/__pycache__/model_config.cpython-314.pyc +0 -0
- package/codex/skill/scripts/__pycache__/apply-models.cpython-314.pyc +0 -0
- package/codex/skill/scripts/__pycache__/model_config.cpython-314.pyc +0 -0
package/lib/policy/placement.mjs
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
const reviews = ['review.significant-no-plan', 'review.significant-plan', 'review.final-plan-recheck'];
|
|
3
3
|
export const POLICY_PROJECTIONS = Object.freeze(Object.fromEntries(Object.entries({
|
|
4
4
|
core: ['scope.non-escalation', 'external.explicit-user-authority', 'role.orchestrator-boundary'],
|
|
5
|
-
assignment: ['git.specialists-deny', 'role.scouter-read-only', 'role.planner-draft-only', 'role.
|
|
6
|
-
planning: ['git.orchestrator-accept-commit', ...reviews],
|
|
7
|
-
handoff: ['git.specialists-deny', 'role.
|
|
8
|
-
preparation: ['scope.non-escalation', 'git.specialists-deny', 'git.orchestrator-accept-commit', 'external.explicit-user-authority'],
|
|
9
|
-
verification: ['git.orchestrator-accept-commit', ...reviews],
|
|
10
|
-
commit: ['scope.non-escalation', 'git.orchestrator-accept-commit', 'external.explicit-user-authority'],
|
|
5
|
+
assignment: ['git.specialists-deny', 'role.scouter-read-only', 'role.planner-draft-only', 'role.implementer-implementation', 'role.qa-verification', 'role.reviewer-independent'],
|
|
6
|
+
planning: ['git.orchestrator-accept-commit', ...reviews, 'planning.plan-amend', 'planning.planner-brief'],
|
|
7
|
+
handoff: ['git.specialists-deny', 'role.implementer-implementation', ...reviews],
|
|
8
|
+
preparation: ['scope.non-escalation', 'git.specialists-deny', 'git.orchestrator-accept-commit', 'external.explicit-user-authority', 'planning.plan-amend'],
|
|
9
|
+
verification: ['git.orchestrator-accept-commit', ...reviews, 'planning.plan-amend'],
|
|
10
|
+
commit: ['scope.non-escalation', 'git.orchestrator-accept-commit', 'external.explicit-user-authority', 'planning.plan-amend'],
|
|
11
11
|
}).map(([name, ids]) => [name, Object.freeze(ids)])));
|
|
12
12
|
|
|
13
13
|
export function validatePolicyProjections(policy, projections = POLICY_PROJECTIONS) {
|
package/lib/policy/render.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import {renderSourceClaudeHook} from './claude-hook.mjs';
|
|
|
2
2
|
import {POLICY_PROJECTIONS, validatePolicyProjections} from './placement.mjs';
|
|
3
3
|
import {validateGitPolicy} from '../../claude/skill/hooks/role-git-guard.mjs';
|
|
4
4
|
import source from './role-policy.json' with {type: 'json'};
|
|
5
|
-
const ROLES = ['orchestrator','scouter','planner','
|
|
5
|
+
const ROLES = ['orchestrator','scouter','planner','implementer','qa','reviewer'];
|
|
6
6
|
const CONTROLS = ['prompt','capability','hook'];
|
|
7
7
|
const fail = message => { throw new Error(`Role policy: ${message}`); };
|
|
8
8
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -18,10 +18,10 @@ export function validateRolePolicy(input) {
|
|
|
18
18
|
if (!strings(rule.roles) || rule.roles.some(x=>!ROLES.includes(x)) || !['allow','deny','require'].includes(rule.effect) || !strings(rule.operations) || !strings(rule.evidence) || !['user','orchestrator'].includes(rule.authority) || typeof rule.text !== 'string' || !rule.text || /[\r\n]/.test(rule.text)) fail(`invalid rule ${rule.id}`);
|
|
19
19
|
if (!object(rule.controls) || !same(Object.keys(rule.controls).sort(),['claude','codex']) || Object.values(rule.controls).some(x=>!CONTROLS.includes(x))) fail(`invalid control level for ${rule.id}`);
|
|
20
20
|
}
|
|
21
|
-
for (const id of ['scope.non-escalation','git.specialists-deny','git.orchestrator-accept-commit','external.explicit-user-authority',...ROLES.map(x=>({orchestrator:'role.orchestrator-boundary',scouter:'role.scouter-read-only',planner:'role.planner-draft-only',
|
|
21
|
+
for (const id of ['scope.non-escalation','git.specialists-deny','git.orchestrator-accept-commit','external.explicit-user-authority',...ROLES.map(x=>({orchestrator:'role.orchestrator-boundary',scouter:'role.scouter-read-only',planner:'role.planner-draft-only',implementer:'role.implementer-implementation',qa:'role.qa-verification',reviewer:'role.reviewer-independent'})[x]),'review.significant-no-plan','review.significant-plan','review.final-plan-recheck']) if(!ids.has(id)) fail(`missing required rule ${id}`);
|
|
22
22
|
for (const platform of ['codex','claude']) if (!object(input.platforms?.[platform]) || !CONTROLS.includes(input.platforms[platform].level) || typeof input.platforms[platform].notice !== 'string' || !input.platforms[platform].notice) fail(`invalid ${platform} controls`);
|
|
23
23
|
// These are workflow invariants, not customizable scheduling options.
|
|
24
|
-
const routes = {noPlan:['
|
|
24
|
+
const routes = {noPlan:['implementer','qa','reviewer','orchestrator-accept'],planTask:['implementer','fresh-qa','orchestrator-accept','orchestrator-commit'],planFinal:['required-qa','reviewer'],planFinding:['implementer','reviewer']};
|
|
25
25
|
if (!object(input.review) || Object.keys(input.review).length !== 4 || Object.entries(routes).some(([key,value])=>!same(input.review[key],value))) fail('invalid review routing');
|
|
26
26
|
try {validateGitPolicy(input.git);} catch {fail('invalid Git policy');}
|
|
27
27
|
if (!object(input.runtime) || input.runtime.schemaVersion !== 1 || !same(input.runtime.roles,ROLES) || !strings(input.runtime.requiredEvidence) || !strings(input.runtime.ruleIds) || ['platform-identity','user-approval','current-target-versions','resolved-paths'].some(x=>!input.runtime.requiredEvidence.includes(x)) || ['runtime.schema','runtime.policy','runtime.identity','runtime.target','runtime.assignment','runtime.approval','runtime.paths','runtime.docs-commit','runtime.mode','runtime.transition'].some(x=>!input.runtime.ruleIds.includes(x)) || input.runtime.status !== 'decision-only') fail('invalid runtime policy');
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"description": "Return design, spec and plan drafts to Orchestrator."
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
"id": "
|
|
17
|
+
"id": "implementer",
|
|
18
18
|
"description": "Implement assigned approved changes and focused checks."
|
|
19
19
|
},
|
|
20
20
|
{
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"orchestrator",
|
|
34
34
|
"scouter",
|
|
35
35
|
"planner",
|
|
36
|
-
"
|
|
36
|
+
"implementer",
|
|
37
37
|
"qa",
|
|
38
38
|
"reviewer"
|
|
39
39
|
],
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"roles": [
|
|
59
59
|
"scouter",
|
|
60
60
|
"planner",
|
|
61
|
-
"
|
|
61
|
+
"implementer",
|
|
62
62
|
"qa",
|
|
63
63
|
"reviewer"
|
|
64
64
|
],
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"orchestrator",
|
|
115
115
|
"scouter",
|
|
116
116
|
"planner",
|
|
117
|
-
"
|
|
117
|
+
"implementer",
|
|
118
118
|
"qa",
|
|
119
119
|
"reviewer"
|
|
120
120
|
],
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"role-ownership"
|
|
151
151
|
],
|
|
152
152
|
"authority": "orchestrator",
|
|
153
|
-
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to
|
|
153
|
+
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Implementer per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the task. Before reusing Implementer, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
|
|
154
154
|
"controls": {
|
|
155
155
|
"codex": "prompt",
|
|
156
156
|
"claude": "prompt"
|
|
@@ -197,9 +197,9 @@
|
|
|
197
197
|
}
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
|
-
"id": "role.
|
|
200
|
+
"id": "role.implementer-implementation",
|
|
201
201
|
"roles": [
|
|
202
|
-
"
|
|
202
|
+
"implementer"
|
|
203
203
|
],
|
|
204
204
|
"effect": "allow",
|
|
205
205
|
"operations": [
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
"criterion-evidence"
|
|
234
234
|
],
|
|
235
235
|
"authority": "orchestrator",
|
|
236
|
-
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for
|
|
236
|
+
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for Implementer correction and required QA recheck.",
|
|
237
237
|
"controls": {
|
|
238
238
|
"codex": "prompt",
|
|
239
239
|
"claude": "prompt"
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
"orchestrator",
|
|
266
266
|
"scouter",
|
|
267
267
|
"planner",
|
|
268
|
-
"
|
|
268
|
+
"implementer",
|
|
269
269
|
"qa",
|
|
270
270
|
"reviewer"
|
|
271
271
|
],
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
"qa-rechecks"
|
|
279
279
|
],
|
|
280
280
|
"authority": "orchestrator",
|
|
281
|
-
"text": "For significant/high-risk work without a plan:
|
|
281
|
+
"text": "For significant/high-risk work without a plan: Implementer \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
|
|
282
282
|
"controls": {
|
|
283
283
|
"codex": "prompt",
|
|
284
284
|
"claude": "prompt"
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
"orchestrator",
|
|
291
291
|
"scouter",
|
|
292
292
|
"planner",
|
|
293
|
-
"
|
|
293
|
+
"implementer",
|
|
294
294
|
"qa",
|
|
295
295
|
"reviewer"
|
|
296
296
|
],
|
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
"required-qa"
|
|
304
304
|
],
|
|
305
305
|
"authority": "orchestrator",
|
|
306
|
-
"text": "For significant/high-risk plan work: each task uses
|
|
306
|
+
"text": "For significant/high-risk plan work: each task uses Implementer \u2192 fresh QA as required \u2192 Orchestrator acceptance and focused commit. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
|
|
307
307
|
"controls": {
|
|
308
308
|
"codex": "prompt",
|
|
309
309
|
"claude": "prompt"
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
"orchestrator",
|
|
316
316
|
"scouter",
|
|
317
317
|
"planner",
|
|
318
|
-
"
|
|
318
|
+
"implementer",
|
|
319
319
|
"qa",
|
|
320
320
|
"reviewer"
|
|
321
321
|
],
|
|
@@ -327,7 +327,52 @@
|
|
|
327
327
|
"reviewer-findings"
|
|
328
328
|
],
|
|
329
329
|
"authority": "orchestrator",
|
|
330
|
-
"text": "For final overall Reviewer findings on plan work, including behavior findings, use
|
|
330
|
+
"text": "For final overall Reviewer findings on plan work, including behavior findings, use Implementer \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
|
|
331
|
+
"controls": {
|
|
332
|
+
"codex": "prompt",
|
|
333
|
+
"claude": "prompt"
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "planning.plan-amend",
|
|
338
|
+
"roles": [
|
|
339
|
+
"orchestrator"
|
|
340
|
+
],
|
|
341
|
+
"effect": "require",
|
|
342
|
+
"operations": [
|
|
343
|
+
"plan-update",
|
|
344
|
+
"plan-commit-amend"
|
|
345
|
+
],
|
|
346
|
+
"evidence": [
|
|
347
|
+
"change-classification",
|
|
348
|
+
"actual-approval-chronology",
|
|
349
|
+
"rewrite-safety",
|
|
350
|
+
"sha-mapping"
|
|
351
|
+
],
|
|
352
|
+
"authority": "orchestrator",
|
|
353
|
+
"text": "During approved task execution, mechanical plan progress records need no new user approval; changed goals, scope, criteria, method, risk, dependencies or required verification require revised-version approval and preparation before affected work. In both cases Orchestrator amends the original planning-document commit at a safe boundary, without a separate document or fixup commit. Preserve actual approval chronology, recoverable old/new contents, hashes, versions and SHA mappings; never retroactively authorize earlier work. Rewrite only unpublished, unshared local linear history with clear ownership and no merges; stop on unknown publication, ownership, unexpected state or replay conflicts, without force push or a fallback document commit. Preserve implementation task boundaries and revalidate affected evidence. See planning, handoff-prep, commit and verification for the full contract.",
|
|
354
|
+
"controls": {
|
|
355
|
+
"codex": "prompt",
|
|
356
|
+
"claude": "prompt"
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"id": "planning.planner-brief",
|
|
361
|
+
"roles": [
|
|
362
|
+
"orchestrator"
|
|
363
|
+
],
|
|
364
|
+
"effect": "require",
|
|
365
|
+
"operations": [
|
|
366
|
+
"planner-brief",
|
|
367
|
+
"planner-feedback"
|
|
368
|
+
],
|
|
369
|
+
"evidence": [
|
|
370
|
+
"complete-stage-brief",
|
|
371
|
+
"prepared-target",
|
|
372
|
+
"preceding-approval"
|
|
373
|
+
],
|
|
374
|
+
"authority": "orchestrator",
|
|
375
|
+
"text": "Before formal drafting, Orchestrator supplies one complete stage-specific brief with original requirements and criteria IDs, approved/open decisions, verified target and ownership, document paths and preceding approved versions, current behavior and connected source/generated/runtime/installer/documentation surfaces, policy conflicts, depth, dependencies, checks, holds and exact deliverable. Preserve shared brief field order. Review the complete draft once and consolidate substantive feedback as changed fields with other fields marked unchanged. Preserve sequential design, spec and plan approvals; no fixed call cap or incomplete drafts, and no calls for already resolved questions.",
|
|
331
376
|
"controls": {
|
|
332
377
|
"codex": "prompt",
|
|
333
378
|
"claude": "prompt"
|
|
@@ -337,7 +382,7 @@
|
|
|
337
382
|
"platforms": {
|
|
338
383
|
"codex": {
|
|
339
384
|
"level": "prompt",
|
|
340
|
-
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified;
|
|
385
|
+
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified; Implementer and QA need shell for approved checks."
|
|
341
386
|
},
|
|
342
387
|
"claude": {
|
|
343
388
|
"level": "capability",
|
|
@@ -346,13 +391,13 @@
|
|
|
346
391
|
},
|
|
347
392
|
"review": {
|
|
348
393
|
"noPlan": [
|
|
349
|
-
"
|
|
394
|
+
"implementer",
|
|
350
395
|
"qa",
|
|
351
396
|
"reviewer",
|
|
352
397
|
"orchestrator-accept"
|
|
353
398
|
],
|
|
354
399
|
"planTask": [
|
|
355
|
-
"
|
|
400
|
+
"implementer",
|
|
356
401
|
"fresh-qa",
|
|
357
402
|
"orchestrator-accept",
|
|
358
403
|
"orchestrator-commit"
|
|
@@ -362,7 +407,7 @@
|
|
|
362
407
|
"reviewer"
|
|
363
408
|
],
|
|
364
409
|
"planFinding": [
|
|
365
|
-
"
|
|
410
|
+
"implementer",
|
|
366
411
|
"reviewer"
|
|
367
412
|
]
|
|
368
413
|
},
|
|
@@ -580,7 +625,7 @@
|
|
|
580
625
|
"orchestrator",
|
|
581
626
|
"scouter",
|
|
582
627
|
"planner",
|
|
583
|
-
"
|
|
628
|
+
"implementer",
|
|
584
629
|
"qa",
|
|
585
630
|
"reviewer"
|
|
586
631
|
],
|
package/lib/release/artifact.mjs
CHANGED
|
@@ -30,7 +30,7 @@ function metadata(pkg,lock,{artifact=false}={}){
|
|
|
30
30
|
if(artifact?root.hasInstallScript!==true:Object.hasOwn(root,'hasInstallScript'))throw new Error('Package lock install script metadata mismatch');
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
function inputNames(pkg){const names=new Set(['README.md','package.json','package-lock.json','MANIFEST.sha256']);for(const entry of pkg.files??[]){const name=entry.replace(/\/$/,'');if(!name||name.startsWith('/')||name.includes('\\')||name.split('/').some(part=>!part||part==='.'||part==='..')||/[*?\[\]]/.test(name))throw new Error('Unsupported package files entry');names.add(name);}return [...names].sort();}
|
|
33
|
+
function inputNames(pkg){const names=new Set(['README.md','package.json','package-lock.json','MANIFEST.sha256']);for(const entry of pkg.files??[]){if(entry==='!**/__pycache__/**'||entry==='!**/*.pyc')continue;const name=entry.replace(/\/$/,'');if(!name||name.startsWith('/')||name.startsWith('!')||name.includes('\\')||name.split('/').some(part=>!part||part==='.'||part==='..')||/[*?\[\]]/.test(name))throw new Error('Unsupported package files entry');names.add(name);}return [...names].sort();}
|
|
34
34
|
function sourceMetadata(root){
|
|
35
35
|
const directory=openRoot(root);
|
|
36
36
|
try{const result={};for(const name of ['package.json','package-lock.json']){const stat=directory.lstat(name);if(typeOf(stat)!==0o100000)throw new Error('Expected regular source metadata: '+name);result[name]=readCheckedFile(directory,name,stat).bytes;}return result;}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {fingerprintRequest} from './store.mjs';
|
|
3
|
+
import {validateGitPreflight,validateGitCommitResult} from './git-evidence.mjs';
|
|
4
|
+
import {within} from './scope.mjs';
|
|
5
|
+
const reconciliations=new WeakMap();
|
|
6
|
+
export function verifiedCooperativeReconciliation(decision,state){const proof=reconciliations.get(decision);return !!proof&&proof.before===fingerprintRequest(state)&&proof.after===fingerprintRequest(decision.proposedTransition);}
|
|
7
|
+
const completed=(state,id)=>state.tasks?.some(t=>t.id===id&&t.status==='committed'&&!t.invalidated)&&state.commits?.some(c=>c.taskId===id&&c.observationSource==='local-git'&&state.intents.some(i=>i.operation==='commit'&&i.requestId===c.requestId&&i.taskId===id&&i.status==='succeeded'));
|
|
8
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
9
|
+
const text=v=>typeof v==='string'&&v.length>0;
|
|
10
|
+
const list=v=>Array.isArray(v)&&v.every(text)&&new Set(v).size===v.length;
|
|
11
|
+
const exact=(a,b)=>list(a)&&list(b)&&same([...a].sort(),[...b].sort());
|
|
12
|
+
const fields=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).every(k=>keys.includes(k));
|
|
13
|
+
const semanticLimit='Semantically unrelated hunks inside the same file require Orchestrator inspection; path and byte evidence cannot distinguish them automatically.';
|
|
14
|
+
const base=state=>({expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},semanticLimit,requiredEvidence:[],nextActions:[],proposedTransition:null});
|
|
15
|
+
const hold=(state,reason)=>({...base(state),decision:'hold',ruleIds:['cooperative.acceptance'],reason});
|
|
16
|
+
const allow=(state,transition)=>({...base(state),decision:'allow',ruleIds:['cooperative.acceptance'],proposedTransition:transition});
|
|
17
|
+
const approval=(state,id)=>state.records.filter(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(id))).at(-1);
|
|
18
|
+
function authorized(state,task){const a=approval(state,task?.id);return state.mode==='plan'&&task&&!task.invalidated&&a?.source==='operator-attested'&&a.targetVersion===state.targetVersion&&task.targetVersion===state.targetVersion&&task.scopeVersion===a.scopeVersion&&task.approvalRef===a.messageRef&&task.approvalVersion===a.data.approvalVersion&&task.documentVersion===a.data.documentVersion;}
|
|
19
|
+
function checkEvidence(state,task,a,changes){
|
|
20
|
+
if(!same(a.criteria,task.criteria)||!task.criteria.length||!list(a.checkIds)||!list(a.findingIds)||a.changeFingerprint!==changes?.tasks?.[task.id])return false;
|
|
21
|
+
const checks=state.checks.filter(c=>c.scope==='task'&&c.taskId===task.id);
|
|
22
|
+
const latest=task.requiredChecks.map(d=>checks.filter(c=>same(c.descriptor,d)).at(-1));
|
|
23
|
+
if(!task.requiredChecks.length||latest.some(c=>!c||c.stale||c.status!=='passed'||c.verdict!=='proven'||!text(c.resultRef)||c.source!=='operator-attested'||c.verificationActor?.role!=='qa'||!text(c.verificationActor.id)||state.assignments.some(a=>a.agentId===c.verificationActor.id)||c.changeFingerprint!==a.changeFingerprint)||!exact(a.checkIds,latest.map(c=>c?.id)))return false;
|
|
24
|
+
const findings=state.findings.filter(f=>f.scope==='task'&&f.taskId===task.id);
|
|
25
|
+
return exact(a.findingIds,findings.map(f=>f.id))&&findings.every(f=>f.status==='closed'&&text(f.resolutionRef)&&text(f.recheckRef)&&f.resolutionActor?.role==='implementer'&&f.recheckActor?.role==='qa'&&f.changeFingerprint===a.changeFingerprint);
|
|
26
|
+
}
|
|
27
|
+
function waitingFor(state,task,predecessor){
|
|
28
|
+
if(task.status!=='pending'||state.intents.some(i=>i.taskId===task.id&&i.operation==='assign-agent'&&['prepared','unknown','mismatch','succeeded'].includes(i.status)))return false;
|
|
29
|
+
const seen=new Set(),queue=[...(task.dependencies??[])];while(queue.length){const id=queue.pop();if(id===predecessor)return true;if(seen.has(id))continue;seen.add(id);queue.push(...(state.tasks.find(t=>t.id===id)?.dependencies??[]));}return false;
|
|
30
|
+
}
|
|
31
|
+
function dirtyOtherTask(state,task,git){return git.stagedPaths?.some(name=>state.tasks.some(t=>t.id!==task.id&&!completed(state,t.id)&&!waitingFor(state,t,task.id)&&t.paths.some(p=>within(p,path.join(state.target.realpath,...name.split('/'))))));}
|
|
32
|
+
function freshAcceptance(state,task,acceptance,changes){return acceptance&&!acceptance.stale&&authorized(state,task)&&acceptance.taskFingerprint===fingerprintRequest({...task,status:'pending'})&&same(acceptance.approval,approval(state,task.id))&&same(acceptance.target,state.target)&&same(acceptance.versions,state.versions)&&checkEvidence(state,task,acceptance,changes);}
|
|
33
|
+
/** Operator attests semantic sufficiency; code binds exact current evidence only. */
|
|
34
|
+
export function evaluateCooperativeAcceptance({state,request,evidence}={}){
|
|
35
|
+
const task=state?.tasks?.find(t=>t.id===request?.taskId),a=request?.arguments,g=evidence?.git;
|
|
36
|
+
if(request?.expectedRevision!==state?.revision||request.actor?.role!=='orchestrator'||!authorized(state,task)||task.status!=='pending'||state.acceptances.some(v=>v.taskId===task.id&&!v.stale)||state.commits.some(v=>v.taskId===task.id))return hold(state,'Current pending approved task and Orchestrator acceptance required');
|
|
37
|
+
if(!fields(a,['messageRef','criteria','checkIds','findingIds','changeFingerprint'])||!text(a.messageRef)||!checkEvidence(state,task,a,evidence?.changes)||state.checks.some(c=>a.checkIds?.includes(c.id)&&c.verificationActor?.id===request.actor.id))return hold(state,'Exact criteria, current required QA checks and closed findings required');
|
|
38
|
+
const descriptor={kind:'task',parent:state.target.head,indexTree:g?.indexTree,fingerprint:g?.stagedFingerprint,paths:g?.stagedPaths};
|
|
39
|
+
const invalid=validateGitPreflight({target:state.target,descriptor,scope:task.paths,gitEvidence:g});
|
|
40
|
+
if(invalid||dirtyOtherTask(state,task,g))return hold(state,invalid??'Index includes another task changes');
|
|
41
|
+
return allow(state,{acceptance:{...structuredClone(a),id:request.requestId,taskId:task.id,actor:structuredClone(request.actor),source:'operator-attested',descriptor:structuredClone(descriptor),taskFingerprint:fingerprintRequest({...task,status:'pending'}),approval:structuredClone(approval(state,task.id)),target:structuredClone(state.target),versions:structuredClone(state.versions)}});
|
|
42
|
+
}
|
|
43
|
+
/** Prepares a durable intent; never stages or executes a commit. */
|
|
44
|
+
export function prepareCooperativeCommit({state,request,gitEvidence,evidence}={}){
|
|
45
|
+
const task=state?.tasks?.find(t=>t.id===request?.taskId),a=state?.acceptances?.find(a=>a.id===request?.arguments?.acceptanceId&&a.taskId===request.taskId);
|
|
46
|
+
if(request?.expectedRevision!==state?.revision||request.actor?.role!=='orchestrator'||!fields(request.arguments,['acceptanceId'])||task?.status!=='accepted'||!freshAcceptance(state,task,a,evidence?.changes)||state.commits.some(c=>c.taskId===task.id)||state.intents.some(i=>i.operation==='commit'&&i.taskId===task.id&&['prepared','unknown','mismatch'].includes(i.status)))return hold(state,'Exact current acceptance and no prior task commit or unresolved commit required');
|
|
47
|
+
const invalid=validateGitPreflight({target:state.target,descriptor:a.descriptor,scope:task.paths,gitEvidence});
|
|
48
|
+
if(invalid||dirtyOtherTask(state,task,gitEvidence))return hold(state,invalid??'Index includes another task changes');
|
|
49
|
+
const expected={parent:a.descriptor.parent,tree:a.descriptor.indexTree,paths:[...a.descriptor.paths],fingerprint:a.descriptor.fingerprint,remainingPaths:[...new Set([...gitEvidence.workingPaths,...gitEvidence.untrackedPaths])].sort()};
|
|
50
|
+
return allow(state,{intent:{requestId:request.requestId,operation:'commit',taskId:task.id,status:'prepared',acceptanceId:a.id,acceptanceFingerprint:fingerprintRequest(a),expected,target:structuredClone(state.target),versions:structuredClone(state.versions),fingerprint:fingerprintRequest(request),source:'operator-attested'}});
|
|
51
|
+
}
|
|
52
|
+
/** Results are local Git observations, correlated with an operator statement.
|
|
53
|
+
* Mismatch observations remain historical evidence and never rebind the target. */
|
|
54
|
+
export function reconcileCooperativeCommit({state,intent,result,gitEvidence}={}){
|
|
55
|
+
if(!intent||intent.operation!=='commit'||!['prepared','unknown','mismatch'].includes(intent.status)||state.commits.some(c=>c.taskId===intent.taskId))return hold(state,'Matching unresolved commit intent required; replay cannot authorize another effect');
|
|
56
|
+
const task=state.tasks.find(t=>t.id===intent.taskId),a=state.acceptances.find(a=>a.id===intent.acceptanceId),g=gitEvidence;
|
|
57
|
+
const observation=g?structuredClone(g):null;
|
|
58
|
+
const transition={observation,result:{...structuredClone(result),status:'mismatch',source:'operator-attested'}};
|
|
59
|
+
const reject=reason=>({...hold(state,reason),proposedTransition:transition});
|
|
60
|
+
if(!result||!text(result.messageRef)||!fields(result.data,['commit'])||!['succeeded','failed','unknown','mismatch'].includes(result.status))return reject('Exact commit result statement required');
|
|
61
|
+
if(result.status!=='succeeded'){transition.result.status=result.status;return reject('Execution did not establish a successful commit');}
|
|
62
|
+
if(task?.status!=='accepted'||!authorized(state,task)||a?.stale||!a||intent.acceptanceFingerprint!==fingerprintRequest(a)||a.taskFingerprint!==fingerprintRequest({...task,status:'pending'})||!same(a.approval,approval(state,task.id))||!checkEvidence(state,task,a,{tasks:{[task.id]:a.changeFingerprint}})||!same(intent.target,state.target)||!same(intent.versions,state.versions)||!g?.commit||result.data.commit!==g.commit.commit||g.stagedPaths?.length||g.indexTree!==g.commit.tree)return reject('Acceptance, observed commit or clean resulting index mismatch');
|
|
63
|
+
const actual={...g.commit,target:g.target,remainingPaths:[...new Set([...g.workingPaths??[],...g.untrackedPaths??[]])].sort()};
|
|
64
|
+
const invalid=validateGitCommitResult({target:state.target,expected:intent.expected,result:actual,scope:task.paths});
|
|
65
|
+
if(invalid)return reject(invalid);
|
|
66
|
+
transition.result.status='succeeded';transition.commit={...actual,taskId:task.id,requestId:intent.requestId,acceptanceId:a.id,source:'operator-attested',observationSource:'local-git',fingerprint:a.changeFingerprint};transition.target=structuredClone(g.target);
|
|
67
|
+
const decision=allow(state,transition);reconciliations.set(decision,{before:fingerprintRequest(state),after:fingerprintRequest(transition)});return decision;
|
|
68
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {fingerprintRequest} from './store.mjs';
|
|
2
|
+
import {absolute,within} from './scope.mjs';
|
|
3
|
+
const completed=(state,id)=>state.tasks?.some(t=>t.id===id&&t.status==='committed'&&!t.invalidated)&&state.commits?.some(c=>c.taskId===id&&c.observationSource==='local-git'&&state.intents.some(i=>i.operation==='commit'&&i.requestId===c.requestId&&i.taskId===id&&i.status==='succeeded'));
|
|
4
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
5
|
+
const text=value=>typeof value==='string'&&value.length>0;
|
|
6
|
+
const version=value=>Number.isSafeInteger(value)&&value>0;
|
|
7
|
+
const setEqual=(a,b)=>Array.isArray(a)&&Array.isArray(b)&&new Set(a).size===a.length&&new Set(b).size===b.length&&same([...a].sort(),[...b].sort());
|
|
8
|
+
const base=state=>({expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},requiredEvidence:[],nextActions:[],proposedTransition:null});
|
|
9
|
+
const hold=(state,id,reason)=>({...base(state),decision:'hold',ruleIds:['cooperative.'+id],reason});
|
|
10
|
+
/** Pure gate. observations are supplied by trusted local readers, never JSON input. */
|
|
11
|
+
export function prepareCooperativeAssignment({policy,state,request,observations}={}){
|
|
12
|
+
if(policy!==undefined&&policy.workflowKind!=='cooperative')return hold(state,'contract','Explicit cooperative contract required');
|
|
13
|
+
const args=request?.arguments;
|
|
14
|
+
if(state?.mode!=='plan'||request?.operation!=='assign-agent'||!args)return hold(state,'unsupported','Only explicit plan assignment is supported');
|
|
15
|
+
const allowed=['role','paths','operations','documents','approvalRef','approvalVersion','documentVersion','correlation'];
|
|
16
|
+
if(Object.keys(args).some(k=>!allowed.includes(k))||args.role!=='implementer'||!text(request.taskId)||!text(request.requestId)||!text(args.correlation)||!text(args.approvalRef)||!version(args.approvalVersion)||!version(args.documentVersion)||!Array.isArray(args.paths)||!args.paths.length||new Set(args.paths).size!==args.paths.length||args.paths.some(p=>!absolute(p)||!within(state.target.realpath,p))||!Array.isArray(args.operations)||!args.operations.length||args.operations.some(o=>o!=='edit'))return hold(state,'assignment','Invalid assignment or unsupported historical agent import');
|
|
17
|
+
if(!same(observations?.target,state.target)||!same(observations?.git?.target,state.target))return hold(state,'target','Re-observe target and renew approval after drift');
|
|
18
|
+
if(request.expectedRevision!==state.revision)return hold(state,'revision','Stale assignment request');
|
|
19
|
+
if(state.records.some(r=>r.data?.priorExecution===true)||state.assignments.some(a=>a.source!=='operator-attested'))return hold(state,'history','Earlier execution remains unverified; a late commit cannot authorize it');
|
|
20
|
+
if(state.intents.some(i=>i.operation==='assign-agent'&&['prepared','unknown','mismatch','succeeded'].includes(i.status)&&(i.taskId===request.taskId||i.correlation===args.correlation||!(i.status==='succeeded'&&completed(state,i.taskId))&&i.paths?.some(p=>args.paths.some(q=>within(p,q)||within(q,p))))))return hold(state,'assignment-active','Resolve the existing dispatch before preparing another');
|
|
21
|
+
const task=state.tasks?.find(t=>t.id===request.taskId);
|
|
22
|
+
if(task&&(task.invalidated||task.status==='held'||task.dependencies?.some(id=>{const predecessor=state.tasks.find(t=>t.id===id);return !predecessor||predecessor.invalidated||!['accepted','committed'].includes(predecessor.status);})))return hold(state,'task-stale','Affected task or predecessor requires renewed readiness and approval');
|
|
23
|
+
const scope=state.scopes?.findLast(s=>s.scopeVersion===state.scopeVersion&&(!s.data.affectedTaskIds||s.data.affectedTaskIds.includes(request.taskId)));
|
|
24
|
+
if(!task||!task.paths?.length||args.paths.some(p=>!task.paths.some(base=>within(base,p)))||scope&&args.paths.some(p=>!scope.data.paths?.some(base=>within(base,p))))return hold(state,'assignment-scope','Assignment must stay inside approved task and recorded scope');
|
|
25
|
+
const documents=args.documents,git=observations.git;
|
|
26
|
+
if(!Array.isArray(documents)||documents.length!==2||!setEqual(documents.map(d=>d.kind),['spec','plan'])||!git.docsCommit||!setEqual(git.docsCommit.paths,documents.map(d=>d.gitPath))||!same(git.docsCommit.documents,documents)||documents.some(d=>d.commit!==git.docsCommit.commit)||!Array.isArray(git.documents)||git.documents.length!==2||documents.some(d=>!git.documents.some(o=>o.path===d.path&&o.gitPath===d.gitPath&&o.kind===d.kind&&o.observedHash===d.contentHash&&o.currentHash===d.contentHash)))return hold(state,'documents','Exact committed spec and plan plus current bytes must match approval');
|
|
27
|
+
const approval=state.records.filter(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(request.taskId))).at(-1);
|
|
28
|
+
if(task.approvalRef!==args.approvalRef||task.approvalVersion!==args.approvalVersion||task.documentVersion!==args.documentVersion||task.scopeVersion!==state.scopeVersion||task.targetVersion!==state.targetVersion||!approval||approval.source!=='operator-attested'||approval.messageRef!==args.approvalRef||approval.targetVersion!==state.targetVersion||approval.scopeVersion!==state.scopeVersion||approval.data.approvalVersion!==args.approvalVersion||approval.data.documentVersion!==args.documentVersion||!same(approval.data.documents,documents)||documents.some(d=>d.approvalRef!==args.approvalRef))return hold(state,'approval','Current document and approval versions must match');
|
|
29
|
+
const intent={requestId:request.requestId,taskId:request.taskId,operation:request.operation,status:'prepared',role:args.role,paths:[...args.paths],operations:[...args.operations],correlation:args.correlation,documents:structuredClone(documents),docsCommit:git.docsCommit.commit,approvalRef:args.approvalRef,approvalVersion:args.approvalVersion,documentVersion:args.documentVersion,target:structuredClone(state.target),targetVersion:state.targetVersion,scopeVersion:state.scopeVersion,revision:state.revision+1,fingerprint:fingerprintRequest(request),source:'operator-attested'};
|
|
30
|
+
return {...base(state),decision:'allow',ruleIds:['cooperative.assignment-prepared'],proposedTransition:{intent}};
|
|
31
|
+
}
|
|
32
|
+
/** A native response is an operator statement, not authenticated identity or compliance. */
|
|
33
|
+
export function recordCooperativeAssignmentResult({state,intent,result,observations}={}){
|
|
34
|
+
if(!intent||intent.operation!=='assign-agent')return hold(state,'intent','No matching assignment intent');
|
|
35
|
+
if(!['prepared','unknown','mismatch'].includes(intent.status))return hold(state,'assignment-terminal','Preserve the prior terminal native response');
|
|
36
|
+
if(!result||!['succeeded','failed','unknown','mismatch'].includes(result.status)||!text(result.messageRef)||!result.data)return hold(state,'assignment-result','Invalid native result statement');
|
|
37
|
+
if(result.status==='succeeded'&&(state.assignments.some(a=>a.agentId===result.data.agentId&&a.taskId!==intent.taskId)||state.intents?.some(i=>i.verificationActor?.id===result.data.agentId)||state.checks?.some(c=>c.verificationActor?.id===result.data.agentId)))return hold(state,'assignment-freshness','Implementer actor statements cannot cross tasks');
|
|
38
|
+
let status=result.status;
|
|
39
|
+
if(!same(intent.target,observations?.target)||result.data.correlation!==intent.correlation||result.data.taskId!==intent.taskId||status==='succeeded'&&!text(result.data.agentId))status='mismatch';
|
|
40
|
+
const entry={...structuredClone(result),status,requestId:intent.requestId,source:'operator-attested'};
|
|
41
|
+
return {...base(state),decision:['succeeded','failed'].includes(status)?'allow':'hold',ruleIds:['cooperative.assignment-'+status],proposedTransition:{result:entry}};
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import {validateCooperativeCommand,validateCooperativeState} from './cooperative-schema.mjs';
|
|
5
|
+
import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workflowStatePath,inspectPath} from './scope.mjs';
|
|
6
|
+
import {saveDurableState} from './store.mjs';
|
|
7
|
+
export const COOPERATIVE_RUNTIME_MODULES=Object.freeze([
|
|
8
|
+
'bootstrap.mjs','cooperative-schema.mjs','cooperative.mjs','evaluate.mjs',
|
|
9
|
+
'scope.mjs','store.mjs','git-evidence.mjs','assignment.mjs','verification.mjs','acceptance.mjs','external.mjs'
|
|
10
|
+
]);
|
|
11
|
+
export const cooperativeCoverage=()=>({enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false});
|
|
12
|
+
export function commandResult({workflowId,state,input,decision='allow',ruleId='cooperative.recorded',persisted=false,...extra}){
|
|
13
|
+
return {decision,ruleIds:[ruleId],workflowId,taskId:input?.taskId??null,requestId:input?.requestId??null,revision:state?.revision??0,coverage:cooperativeCoverage(),requiredEvidence:[],nextActions:[],persisted,...extra};
|
|
14
|
+
}
|
|
15
|
+
export async function cooperativeVersions(){
|
|
16
|
+
const hash=bytes=>'sha256:'+crypto.createHash('sha256').update(bytes).digest('hex');
|
|
17
|
+
let policy;try{policy=await fs.readFile(new URL('../policy/role-policy.json',import.meta.url));}catch(error){if(error.code!=='ENOENT')throw error;policy=await fs.readFile(new URL('./role-policy.json',import.meta.url));}
|
|
18
|
+
const runtime=await Promise.all(COOPERATIVE_RUNTIME_MODULES.map(file=>fs.readFile(new URL(file,import.meta.url))));
|
|
19
|
+
return {policy:'1',runtime:'cooperative-1',policyHash:hash(policy),runtimeHash:hash(Buffer.concat(runtime))};
|
|
20
|
+
}
|
|
21
|
+
export async function initializeCooperativeWorkflow({cwd,workflowId,input,observe=observeCooperativeTarget,storage={}}){
|
|
22
|
+
validateWorkflowId(workflowId);validateCooperativeCommand({command:'init',input});
|
|
23
|
+
if(input.mode!=='plan')return commandResult({workflowId,input,decision:'hold',ruleId:'cooperative.unsupported'});
|
|
24
|
+
const target=await validateObservedTarget(cwd,await observe(cwd));const statePath=await workflowStatePath(target,workflowId);
|
|
25
|
+
const parent=path.dirname(statePath);await fs.mkdir(parent,{recursive:true,mode:0o700});await inspectPath(parent,{directory:true});
|
|
26
|
+
const lockPath=statePath+'.lock';let lock;
|
|
27
|
+
try{
|
|
28
|
+
lock=await fs.open(lockPath,'wx',0o600);await inspectPath(statePath,{missing:true});
|
|
29
|
+
const state={schemaVersion:1,workflowKind:'cooperative',contractVersion:1,workflowId,revision:0,mode:'plan',coverage:cooperativeCoverage(),versions:await cooperativeVersions(),target,targetVersion:1,scopeVersion:0};
|
|
30
|
+
for(const key of ['actors','selections','assignments','identities','leases','scopes','approvals','documents','tasks','requests','intents','results','checks','findings','acceptances','commits','audit','unresolved','records','targetHistory'])state[key]=[];
|
|
31
|
+
state.actors.push({actor:input.actor,source:'operator-attested'});state.selections.push({...input.selection,actor:input.actor,source:'operator-attested',targetVersion:1});
|
|
32
|
+
state.audit.push({command:'init',revision:0,source:'operator-attested',actor:input.actor});validateCooperativeState(state);
|
|
33
|
+
await saveDurableState({statePath,state,create:true,storage});return commandResult({workflowId,state,input,persisted:true,ruleId:'cooperative.initialized'});
|
|
34
|
+
}finally{if(lock){await lock.close();await fs.unlink(lockPath);}}
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {ROLES} from './operations.mjs';
|
|
2
|
+
import {assertSecretFree} from './external.mjs';
|
|
3
|
+
import {object,text} from './schema.mjs';
|
|
4
|
+
import {absolute,validateWorkflowId} from './scope.mjs';
|
|
5
|
+
const fail=message=>{throw Error('Cooperative schema: '+message);};
|
|
6
|
+
const hash=value=>typeof value==='string'&&/^sha256:[a-f0-9]{64}$/.test(value);
|
|
7
|
+
const revision=value=>Number.isSafeInteger(value)&&value>=0;
|
|
8
|
+
const actor=value=>object(value)&&text(value.id)&&Object.keys(value).every(k=>['id','role'].includes(k))&&(value.role===undefined||ROLES.includes(value.role));
|
|
9
|
+
const statement=value=>object(value)&&text(value.messageRef)&&object(value.data);
|
|
10
|
+
function fields(value,allowed){if(!object(value)||Object.keys(value).some(k=>!allowed.includes(k)))fail('unexpected input field');}
|
|
11
|
+
function json(value){try{const encoded=JSON.stringify(value);if(encoded===undefined)fail('JSON input');}catch{fail('JSON input');}}
|
|
12
|
+
export function validateCooperativeCommand(command){
|
|
13
|
+
assertSecretFree(command);fields(command,['command','input']);const {input,command:name}=command;
|
|
14
|
+
if(!['init','record','status','reconcile','prepare','result'].includes(name))fail('command');
|
|
15
|
+
if(name==='status'){if(input!==undefined)fail('status takes no input');return command;}
|
|
16
|
+
const resultStatement=name==='reconcile'||name==='result'&&input?.operation===undefined;
|
|
17
|
+
const allowed=name==='init'?['mode','actor','selection']:name==='record'?['requestId','expectedRevision','actor','evidence']:resultStatement?['requestId','expectedRevision','actor','intentId','result']:['requestId','expectedRevision','actor','operation','taskId','arguments'];
|
|
18
|
+
fields(input,allowed);json(input);
|
|
19
|
+
if(!actor(input.actor))fail('actor attestation');
|
|
20
|
+
if(name==='init'){
|
|
21
|
+
if(!['plan','no-plan','small-task'].includes(input.mode)||!object(input.selection)||!text(input.selection.messageRef)||!hash(input.selection.contentHash)||Object.keys(input.selection).some(k=>!['messageRef','contentHash'].includes(k)))fail('target selection');
|
|
22
|
+
return command;
|
|
23
|
+
}
|
|
24
|
+
if(!text(input.requestId)||input.requestId.length>128||!revision(input.expectedRevision))fail('request identity or revision');
|
|
25
|
+
if(name==='record'){
|
|
26
|
+
fields(input.evidence,['kind','messageRef','data']);
|
|
27
|
+
if(input.evidence?.data?.affectedTaskIds!==undefined&&(!Array.isArray(input.evidence.data.affectedTaskIds)||!input.evidence.data.affectedTaskIds.every(text)||new Set(input.evidence.data.affectedTaskIds).size!==input.evidence.data.affectedTaskIds.length))fail('affected task IDs');
|
|
28
|
+
if(!statement(input.evidence)||!['note','approval','external-approval','document','scope','check','finding','review','acceptance-evidence'].includes(input.evidence.kind))fail('evidence');
|
|
29
|
+
}
|
|
30
|
+
if(resultStatement){
|
|
31
|
+
fields(input.result,['status','messageRef','data','notExecutedEvidence']);
|
|
32
|
+
if(!text(input.intentId)||!statement(input.result)||!['succeeded','failed','unknown','mismatch','cancelled'].includes(input.result.status))fail('result evidence');
|
|
33
|
+
if(input.result.status==='cancelled'&&(!object(input.result.notExecutedEvidence)||!text(input.result.notExecutedEvidence.messageRef)))fail('cancel requires not-executed evidence');
|
|
34
|
+
}
|
|
35
|
+
if((name==='prepare'||name==='result'&&!resultStatement)&&!text(input.operation))fail('operation');
|
|
36
|
+
return command;
|
|
37
|
+
}
|
|
38
|
+
export function validateCooperativeState(state){
|
|
39
|
+
if(!object(state)||state.workflowKind!=='cooperative'||state.contractVersion!==1||state.schemaVersion!==1||!revision(state.revision)||state.mode!=='plan')fail('explicit cooperative contract');
|
|
40
|
+
validateWorkflowId(state.workflowId);
|
|
41
|
+
if(!object(state.target)||!absolute(state.target.realpath)||!absolute(state.target.gitCommonDir)||!absolute(state.target.gitDir)||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(state.target.head)||!(state.target.branch===null||text(state.target.branch)))fail('target');
|
|
42
|
+
if(!object(state.versions)||!text(state.versions.policy)||!text(state.versions.runtime)||!hash(state.versions.policyHash)||!hash(state.versions.runtimeHash))fail('versions');
|
|
43
|
+
if(!object(state.coverage)||state.coverage.provenance!=='operator-attested'||state.coverage.nativeVerified!==false||state.coverage.enforcement!=='cooperative')fail('coverage');
|
|
44
|
+
for(const key of ['actors','selections','assignments','identities','leases','scopes','approvals','documents','tasks','requests','intents','results','checks','findings','acceptances','commits','audit','unresolved','records','targetHistory'])if(!Array.isArray(state[key]))fail(key);
|
|
45
|
+
if(state.tasks.some(t=>!object(t)||!text(t.id)||!Array.isArray(t.paths)||!Array.isArray(t.criteria)||!Array.isArray(t.requiredChecks)||!Array.isArray(t.dependencies))||new Set(state.tasks.map(t=>t.id)).size!==state.tasks.length)fail('task contracts');
|
|
46
|
+
if(state.commitObservations!==undefined&&(!Array.isArray(state.commitObservations)||state.commitObservations.some(o=>!object(o)||o.source!=='local-git'||!text(o.intentId)||!revision(o.revision))))fail('commit observations');
|
|
47
|
+
if(!revision(state.scopeVersion)||!revision(state.targetVersion))fail('scope/target version');
|
|
48
|
+
if(state.actors.some(a=>!actor(a.actor)||a.source!=='operator-attested')||state.selections.some(s=>s.source!=='operator-attested'||!text(s.messageRef)))fail('attestations');
|
|
49
|
+
for(const key of ['scopes','approvals','documents','checks','findings','acceptances','commits'])if(state[key].some(entry=>!object(entry)||entry.source!=='operator-attested'))fail('attestation source');
|
|
50
|
+
if(state.records.some(r=>!actor(r.actor)||r.source!=='operator-attested'||!text(r.kind)||!statement(r)))fail('records');
|
|
51
|
+
if(state.requests.some(r=>!text(r.requestId)||!hash(r.requestFingerprint)||!object(r.decision))||new Set(state.requests.map(r=>r.requestId)).size!==state.requests.length)fail('request replay');
|
|
52
|
+
if(state.intents.some(i=>!text(i.requestId)||!['prepared','succeeded','failed','unknown','mismatch','cancelled'].includes(i.status)||!hash(i.fingerprint))||new Set(state.intents.map(i=>i.requestId)).size!==state.intents.length)fail('intents');
|
|
53
|
+
if(state.results.some(r=>!text(r.requestId)||!['succeeded','failed','unknown','mismatch','cancelled'].includes(r.status)||r.source!=='operator-attested'||!object(r.data)))fail('results');
|
|
54
|
+
assertSecretFree(state);json(state);return state;
|
|
55
|
+
}
|