zob-harness 0.13.0 → 0.14.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/.pi/capabilities/zob-public-runtime-capabilities.json +1 -1
- package/.pi/extensions/zob-harness/index.ts +73 -0
- package/.pi/extensions/zob-harness/src/domains/capability/AGENTS.md +72 -0
- package/.pi/extensions/zob-harness/src/domains/capability/capability-contract.ts +121 -0
- package/.pi/extensions/zob-harness/src/domains/capability/launch-gate.ts +185 -0
- package/.pi/extensions/zob-harness/src/domains/capability/nudge-policy.ts +397 -0
- package/.pi/extensions/zob-harness/src/domains/capability/primitives.ts +192 -0
- package/.pi/extensions/zob-harness/src/domains/capability/types.ts +94 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/envelope.ts +22 -1
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/local-transport.ts +23 -4
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/local-transport.ts.bak.1781340854 +147 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +127 -2
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +90 -21
- package/.pi/extensions/zob-harness/src/runtime/commands/zlive.ts +80 -13
- package/.pi/extensions/zob-harness/src/runtime/events.ts +213 -47
- package/.pi/extensions/zob-harness/src/runtime/schemas.ts +4 -1
- package/.pi/extensions/zob-harness/src/runtime/state.ts +20 -3
- package/.pi/extensions/zob-harness/src/runtime/tools-coms.ts +38 -15
- package/.pi/prompts/implement.md +1 -0
- package/.pi/prompts/orchestrator.md +2 -1
- package/.pi/skills/zob-coms-safety/SKILL.md +5 -0
- package/.pi/skills/zob-coms-v2-live/SKILL.md +4 -0
- package/.pi/skills/zob-goal-todo-tree/SKILL.md +1 -1
- package/.pi/skills/zob-harness/SKILL.md +1 -1
- package/package.json +1 -1
- package/scripts/context-discovery/query.mjs +9 -2
- package/scripts/zpeer-local-e2e-smoke.mjs +29 -2
- package/scripts/zpeer-static-smoke.mjs +9 -3
|
@@ -719,7 +719,7 @@
|
|
|
719
719
|
"docRefs": [
|
|
720
720
|
".pi/extensions/zob-harness/src/AGENTS.md"
|
|
721
721
|
],
|
|
722
|
-
"noShipNotes": "Agent-initiated visible ZPeer ask; local_socket only, room-scoped with optional explicit roomId, mode defaults async, rate/loop guarded, raw bodies transient and durable records hash-only."
|
|
722
|
+
"noShipNotes": "Agent-initiated visible ZPeer ask; local_socket only, room-scoped with optional explicit roomId, mode defaults async, normal/urgent/force are rate/loop guarded, force requires a hashed reason and role/topology guards, raw bodies transient and durable records hash-only."
|
|
723
723
|
},
|
|
724
724
|
{
|
|
725
725
|
"name": "zob_goal_room_send",
|
|
@@ -352,6 +352,79 @@ export type {
|
|
|
352
352
|
AutoResolveVerdict,
|
|
353
353
|
ResolutionStrategyFn,
|
|
354
354
|
} from "./src/domains/environment/auto-resolve.js";
|
|
355
|
+
// WS-CH1 (capability-validation PART II keystone): the typed CapabilityContract
|
|
356
|
+
// pillar (the 5th pillar alongside computeWorklist/EvidenceContract/EnvironmentContract).
|
|
357
|
+
// The contract shape + registry + PURE primitives (no node:fs/node:child_process); the
|
|
358
|
+
// body + readManifest IO + the role->required-tools map are project-registered (WS-CH3).
|
|
359
|
+
// Metadata-only / body-free / network-disabled: AgentManifest/RoleRequirement/
|
|
360
|
+
// CapabilityVerdict carry agent ids, tool names, mode names, manifest paths, fixCommand
|
|
361
|
+
// strings only; FORBIDDEN_PLAINTEXT_KEYS applies (reused from the worklist domain).
|
|
362
|
+
// CRITICAL SAFETY: NO auto-resolve on the contract (manifest edit = security-sensitive =
|
|
363
|
+
// operator-gated, unlike Round 4 env auto-resolve).
|
|
364
|
+
export {
|
|
365
|
+
buildFixPacket,
|
|
366
|
+
compareCapability,
|
|
367
|
+
manifestHasTool,
|
|
368
|
+
modePermitsWrite,
|
|
369
|
+
requiredToolsForRole,
|
|
370
|
+
} from "./src/domains/capability/primitives.js";
|
|
371
|
+
export type { CapabilityFixPacketEntry } from "./src/domains/capability/primitives.js";
|
|
372
|
+
export {
|
|
373
|
+
capabilityBodyFreeViolations,
|
|
374
|
+
listCapabilityContractIds,
|
|
375
|
+
registerCapabilityContract,
|
|
376
|
+
resolveCapabilityContract,
|
|
377
|
+
} from "./src/domains/capability/capability-contract.js";
|
|
378
|
+
export type { CapabilityContract } from "./src/domains/capability/capability-contract.js";
|
|
379
|
+
export type {
|
|
380
|
+
AgentManifest,
|
|
381
|
+
CapabilityVerdict,
|
|
382
|
+
RoleName,
|
|
383
|
+
RoleRequirement,
|
|
384
|
+
} from "./src/domains/capability/types.js";
|
|
385
|
+
// WS-CH2 (capability-validation PART II): the launch-time gate primitive + the
|
|
386
|
+
// runtime nudge-backoff/gap primitives. runCapabilityGate reads each manifest once
|
|
387
|
+
// (via the contract's readManifest), evaluates every agent against its role's
|
|
388
|
+
// requirement, returns { ok, verdicts, fix_packet, shouldStart } with
|
|
389
|
+
// shouldStart === ok BY CONSTRUCTION (fail-closed, no opt-out). Pure over
|
|
390
|
+
// (contract, agentIds); never starts anything itself (mechanism in harness, action
|
|
391
|
+
// in app). Idempotent + re-runnable (re-reads manifests on re-call). The nudge-
|
|
392
|
+
// policy primitives compose the supervisor anti-spam policy: planBackoffNudge
|
|
393
|
+
// (60s→2m→5m→15m cap + the structural capability_gap_stop), detectCapabilityGap
|
|
394
|
+
// (gates escalation — slow-but-capable stays gap===false), capabilityGapFixPacket
|
|
395
|
+
// (the metadata-only alert_no_ship fix packet), transitionOnCapabilityGap (forces
|
|
396
|
+
// the terminal capability_gap:true stop once gap===true). Metadata-only /
|
|
397
|
+
// body-free / network-disabled. No node:fs/node:child_process (the purity grep
|
|
398
|
+
// returns nothing). CRITICAL SAFETY: NO auto-resolve (manifest edit = security-
|
|
399
|
+
// sensitive = operator-gated, intentional divergence from Round 4 env auto-resolve).
|
|
400
|
+
export { runCapabilityGate } from "./src/domains/capability/launch-gate.js";
|
|
401
|
+
// NOTE: aliased as CapabilityLaunchGateOptions / CapabilityLaunchGateResult to
|
|
402
|
+
// avoid a name collision with the WS-PH2 environment pillar's identically-named
|
|
403
|
+
// LaunchGateOptions / LaunchGateResult exports above (index.ts ~L330). Both
|
|
404
|
+
// shapes exist; consumers pick the capability gate via the `Capability*` prefix.
|
|
405
|
+
export type {
|
|
406
|
+
LaunchGateOptions as CapabilityLaunchGateOptions,
|
|
407
|
+
LaunchGateResult as CapabilityLaunchGateResult,
|
|
408
|
+
} from "./src/domains/capability/launch-gate.js";
|
|
409
|
+
export {
|
|
410
|
+
DEFAULT_NUDGE_SCHEDULE,
|
|
411
|
+
NUDGE_BACKOFF_CAP_MS,
|
|
412
|
+
capabilityGapFixPacket,
|
|
413
|
+
detectCapabilityGap,
|
|
414
|
+
planBackoffNudge,
|
|
415
|
+
transitionOnCapabilityGap,
|
|
416
|
+
} from "./src/domains/capability/nudge-policy.js";
|
|
417
|
+
export type {
|
|
418
|
+
CapabilityGapAction,
|
|
419
|
+
CapabilityGapFixPacket,
|
|
420
|
+
CapabilityGapTransition,
|
|
421
|
+
DetectCapabilityGapInput,
|
|
422
|
+
DriverRecord,
|
|
423
|
+
GapResult,
|
|
424
|
+
NudgePlan,
|
|
425
|
+
NudgeSchedule,
|
|
426
|
+
PlanBackoffNudgeInput,
|
|
427
|
+
} from "./src/domains/capability/nudge-policy.js";
|
|
355
428
|
export { DEFAULT_PROMOTION_GATES, advancePromotionCandidate, appendPromotionLedger, createPromotionCandidate, promotionCandidateDir, promotionCandidateRef, promotionReportsDir, summarizePromotionCandidates, transitionAllowed, validatePromotionCandidate, writePromotionCandidate } from "./src/domains/promotion/candidate.js";
|
|
356
429
|
export { addPromotionComsMessageRef, buildPromotionComsMessageRef, buildPromotionComsThread, validatePromotionComsMessageRef, validatePromotionComsReadiness, validatePromotionComsThread, writePromotionComsThread } from "./src/domains/promotion/coms.js";
|
|
357
430
|
export { applyDocumentationPromotionInQuarantine, prepareDocumentationPromotion, validateDocumentationPromotion, validateDocumentationPromotionCandidate } from "./src/domains/promotion/documentation.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Capability domain (`src/domains/capability/`)
|
|
2
|
+
|
|
3
|
+
WS-CH1 keystone of the capability-validation extraction (PART II). The FIFTH
|
|
4
|
+
harness pillar alongside `computeWorklist` (Round 2), `EvidenceContract`
|
|
5
|
+
(Round 3), and `EnvironmentContract` (Round 4): a typed `CapabilityContract` +
|
|
6
|
+
registry + PURE check primitives that shift agent-capability validation (an
|
|
7
|
+
agent's `allowedTools`/`defaultMode` vs the protocol's per-role required tools)
|
|
8
|
+
from late runtime discovery (a stall + 59 nudge-spams when a structurally
|
|
9
|
+
incapable agent is launched) to an early fail-closed launch gate every consumer
|
|
10
|
+
gets for free.
|
|
11
|
+
|
|
12
|
+
## Shape provenance
|
|
13
|
+
|
|
14
|
+
- `AgentManifest` / `RoleRequirement` / `CapabilityVerdict` / `CapabilityContract`
|
|
15
|
+
mirror the WS-CH1 plan signatures (adapted JSDoc typedefs -> TS interfaces;
|
|
16
|
+
camelCase harness convention) verbatim. `registerCapabilityContract` /
|
|
17
|
+
`resolveCapabilityContract` / `listCapabilityContractIds` Map registry mirrors
|
|
18
|
+
`environment-contract.ts` (Round 4) EXACTLY.
|
|
19
|
+
- The registry is keyed by `reducerId` (projects register under their
|
|
20
|
+
`reducer_id`; the transposer registers `reducerId='project-transposer'` in
|
|
21
|
+
WS-CH3). A missing contract resolves to `undefined` (typed-missing, NOT a
|
|
22
|
+
silent default) — same plan-specified divergence as `resolveEnvironmentContract`.
|
|
23
|
+
|
|
24
|
+
## Purity contract (headline acceptance)
|
|
25
|
+
|
|
26
|
+
- `primitives.ts` imports NO `node:fs`, NO `node:child_process`, NO
|
|
27
|
+
`spawnSync`/`readdirSync`/`readFileSync`/`exec`. It is PURE over the
|
|
28
|
+
`AgentManifest` / `RoleRequirement` / `CapabilityVerdict` data structures the
|
|
29
|
+
caller passes (the ONLY IO seam is the project-registered `readManifest`, which
|
|
30
|
+
the harness never calls — it is a signature only in CH1).
|
|
31
|
+
- `types.ts` + `capability-contract.ts` import ONLY `src/core/**` + siblings
|
|
32
|
+
(`./types.js`, `../worklist/types.js` for `FORBIDDEN_PLAINTEXT_KEYS`). No
|
|
33
|
+
runtime; no `@earendil-works/pi-coding-agent`. This keeps the domain reusable by
|
|
34
|
+
transposer/pacman-style projections.
|
|
35
|
+
|
|
36
|
+
## MUST DO
|
|
37
|
+
|
|
38
|
+
- Keep the contract SHAPE-ONLY; the body (`evaluateCapability` dispatch +
|
|
39
|
+
`readManifest` IO + the role->required-tools map) is project-registered.
|
|
40
|
+
`readManifest` is the ONLY IO and is project-supplied (the harness defines the
|
|
41
|
+
signature only). Primitives stay pure over the inputs. Metadata-only /
|
|
42
|
+
body-free / network-disabled: agent ids, tool names, mode names, manifest paths,
|
|
43
|
+
fixCommand strings only.
|
|
44
|
+
|
|
45
|
+
## MUST NOT
|
|
46
|
+
|
|
47
|
+
- No `node:fs` / `node:child_process` (or `spawnSync`/`readdirSync`/`exec`) in
|
|
48
|
+
`primitives.ts`. No raw bodies/prompts/diffs/secrets in any manifest,
|
|
49
|
+
requirement, verdict, or fix packet. No network. No change to the worklist or
|
|
50
|
+
environment domains (read-only import of `FORBIDDEN_PLAINTEXT_KEYS`).
|
|
51
|
+
- **CRITICAL SAFETY DIVERGENCE from Round 4**: NO auto-resolve method on the
|
|
52
|
+
contract (no `applyAutoResolve`, no auto-fix). Editing a ZAgent manifest changes
|
|
53
|
+
an agent's authority (allowedTools/defaultMode) = SECURITY-SENSITIVE = always
|
|
54
|
+
operator-gated, unlike Round 4's reversible environment auto-resolve. The
|
|
55
|
+
contract evaluates + surfaces a fix packet; applying the edit is app/operator-side.
|
|
56
|
+
|
|
57
|
+
## Files
|
|
58
|
+
|
|
59
|
+
- `types.ts` — `RoleName`, `AgentManifest`, `RoleRequirement`, `CapabilityVerdict`
|
|
60
|
+
(all readonly, metadata-only).
|
|
61
|
+
- `capability-contract.ts` — `CapabilityContract`, `registerCapabilityContract` /
|
|
62
|
+
`resolveCapabilityContract` / `listCapabilityContractIds` Map registry,
|
|
63
|
+
`capabilityBodyFreeViolations`.
|
|
64
|
+
- `primitives.ts` — `manifestHasTool`, `modePermitsWrite`, `requiredToolsForRole`,
|
|
65
|
+
`compareCapability`, `buildFixPacket` (PURE over the inputs; result interface
|
|
66
|
+
`CapabilityFixPacketEntry` exported).
|
|
67
|
+
|
|
68
|
+
## Validation
|
|
69
|
+
|
|
70
|
+
- `npm run check -- --pretty false` clean; `node --import tsx --test "test/capability/*"` green.
|
|
71
|
+
- `grep primitives.ts for direct fs / process-spawn / exec tokens` returns
|
|
72
|
+
NOTHING (the purity proof).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ZOB Harness — Agent capability validation contract + registry (WS-CH1 keystone,
|
|
2
|
+
// capability-validation PART II).
|
|
3
|
+
//
|
|
4
|
+
// The FIFTH PILLAR alongside computeWorklist (Round 2), EvidenceContract
|
|
5
|
+
// (Round 3), and EnvironmentContract (Round 4): a typed CapabilityContract that
|
|
6
|
+
// every multi-agent consumer registers under its `reducer_id`, mirroring
|
|
7
|
+
// registerEnvironmentContract's Map registry EXACTLY. The harness ships the SHAPE
|
|
8
|
+
// (the contract interface + the pure primitives) + the registry; the BODY
|
|
9
|
+
// (which roles need which tools) + the manifest READER (`readManifest` IO) are
|
|
10
|
+
// project-registered (WS-CH3, deferred). CH1-style: shape-only, deferred body.
|
|
11
|
+
//
|
|
12
|
+
// CRITICAL SAFETY DIVERGENCE from Round-4 EnvironmentContract (documented here so
|
|
13
|
+
// it is never accidentally re-introduced): there is NO auto-resolve method on
|
|
14
|
+
// this contract. Round 4's EnvironmentContract had `snapshotEnvironment` + a
|
|
15
|
+
// WS-PH3 `applyAutoResolve` framework (allowlisted + REVERSIBLE env fixes). A
|
|
16
|
+
// capability gap is NOT auto-resolvable: editing a ZAgent manifest changes an
|
|
17
|
+
// agent's authority (its allowedTools/defaultMode), which is a SECURITY-SENSITIVE
|
|
18
|
+
// operation that must stay operator-gated. This contract therefore exposes ONLY
|
|
19
|
+
// `evaluateCapability` (pure verdict) + `readManifest` (project-supplied IO) +
|
|
20
|
+
// `requirements` (project-supplied role→required-tools body). The fix packet the
|
|
21
|
+
// verdict surfaces is the END of what the harness does; applying the manifest edit
|
|
22
|
+
// is app/operator-side (WS-CH2 launch-gate will turn a failing verdict into a
|
|
23
|
+
// fail-closed block + a printed fix_command, never an auto-edit). This is the
|
|
24
|
+
// same split as Round 4's `runLaunchGate` (mechanism in harness, action in app)
|
|
25
|
+
// EXCEPT there is intentionally no `applyAutoResolve` sibling — the action is
|
|
26
|
+
// always a human edit.
|
|
27
|
+
//
|
|
28
|
+
// Purity contract: imports ONLY from src/core/** + siblings (../worklist/types.js
|
|
29
|
+
// for FORBIDDEN_PLAINTEXT_KEYS, ./types.js for the typed shapes — type-only,
|
|
30
|
+
// erased at runtime). No IO; no runtime; no @earendil-works/pi-coding-agent
|
|
31
|
+
// types. Metadata-only / body-free / network-disabled: CapabilityContract
|
|
32
|
+
// consumers carry agent ids, tool names, mode names, manifest paths, and
|
|
33
|
+
// fixCommand strings only; FORBIDDEN_PLAINTEXT_KEYS is enforced on every value
|
|
34
|
+
// that enters the contract via capabilityBodyFreeViolations.
|
|
35
|
+
import { FORBIDDEN_PLAINTEXT_KEYS } from "../worklist/types.js";
|
|
36
|
+
import type {
|
|
37
|
+
AgentManifest,
|
|
38
|
+
CapabilityVerdict,
|
|
39
|
+
RoleName,
|
|
40
|
+
RoleRequirement,
|
|
41
|
+
} from "./types.js";
|
|
42
|
+
|
|
43
|
+
// --- The capability contract ------------------------------------------------
|
|
44
|
+
// Project-registered under its `reducer_id` (the same keying as
|
|
45
|
+
// EnvironmentContract / EvidenceContract / WorklistReducer). `evaluateCapability`
|
|
46
|
+
// is the SINGLE SITE of capability satisfaction: pure over
|
|
47
|
+
// (manifest, requirement); no IO. `readManifest` is the ONLY IO entry point —
|
|
48
|
+
// the project supplies it (reads .pi/zagents/<id>.json -> AgentManifest); CH1
|
|
49
|
+
// defines the signature only and never does IO in the harness. `requirements`
|
|
50
|
+
// returns the project's role→required-tools body (from the protocol's
|
|
51
|
+
// machine-readable `Required tools per role` section / .pi/capabilities/<id>.json).
|
|
52
|
+
export interface CapabilityContract {
|
|
53
|
+
/** Canonical verdict, pure over (manifest, requirement). */
|
|
54
|
+
evaluateCapability(manifest: AgentManifest, requirement: RoleRequirement): CapabilityVerdict;
|
|
55
|
+
/** Project-supplied manifest reader (.pi/zagents/<id>.json -> AgentManifest). The ONLY IO. */
|
|
56
|
+
readManifest(agentId: string): AgentManifest;
|
|
57
|
+
/** Project-supplied role->required-tools body (from protocol / .pi/capabilities). */
|
|
58
|
+
requirements(): Record<RoleName, RoleRequirement>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Body-free enforcement ---------------------------------------------------
|
|
62
|
+
// Mirrors environmentBodyFreeViolations / evidenceBodyFreeViolations /
|
|
63
|
+
// worklistBodyFreeViolations: recursively scan an AgentManifest / RoleRequirement
|
|
64
|
+
// / CapabilityVerdict / fix-packet value and report any FORBIDDEN_PLAINTEXT_KEYS
|
|
65
|
+
// found (body/task/prompt/output/content/message/text/rationale/diff/patch).
|
|
66
|
+
// Returns [] for a clean value. The launch gate (WS-CH2) will call this before
|
|
67
|
+
// threading a manifest set into the contract; CH1 exposes it now so the contract
|
|
68
|
+
// is body-free from the first use.
|
|
69
|
+
export function capabilityBodyFreeViolations(value: unknown): string[] {
|
|
70
|
+
const violations: string[] = [];
|
|
71
|
+
const visit = (item: unknown, path: string): void => {
|
|
72
|
+
if (!item || typeof item !== "object") return;
|
|
73
|
+
if (Array.isArray(item)) {
|
|
74
|
+
for (let index = 0; index < item.length; index += 1) visit(item[index], `${path}[${index}]`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const [key, child] of Object.entries(item as Record<string, unknown>)) {
|
|
78
|
+
if (FORBIDDEN_PLAINTEXT_KEYS.has(key)) violations.push(`${path}.${key}`);
|
|
79
|
+
visit(child, `${path}.${key}`);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
visit(value, "root");
|
|
83
|
+
return violations;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Capability contract registry -------------------------------------------
|
|
87
|
+
// Mirrors the EnvironmentContract registry (environment-contract.ts
|
|
88
|
+
// registerEnvironmentContract / resolveEnvironmentContract /
|
|
89
|
+
// listEnvironmentContractIds) and the EvidenceContract / WorklistReducer
|
|
90
|
+
// registries: keyed by reducerId (projects register under their reducer_id; the
|
|
91
|
+
// transposer will register reducerId='project-transposer' in WS-CH3). A MISSING
|
|
92
|
+
// contract resolves to `undefined` — a typed-missing signal, NOT a silent
|
|
93
|
+
// default — so the caller can fail-closed explicitly. (Same deliberate,
|
|
94
|
+
// plan-specified divergence as resolveEnvironmentContract: the WS-CH1 plan
|
|
95
|
+
// signature is `CapabilityContract | undefined`; the launch gate WS-CH2 will turn
|
|
96
|
+
// undefined into an explicit fail-closed verdict.)
|
|
97
|
+
const CAPABILITY_CONTRACTS = new Map<string, CapabilityContract>();
|
|
98
|
+
|
|
99
|
+
export function registerCapabilityContract(
|
|
100
|
+
reducerId: string,
|
|
101
|
+
contract: CapabilityContract,
|
|
102
|
+
): void {
|
|
103
|
+
if (!reducerId) throw new Error("registerCapabilityContract: reducerId is required");
|
|
104
|
+
if (!contract || typeof contract !== "object")
|
|
105
|
+
throw new Error(`registerCapabilityContract('${reducerId}'): contract is required`);
|
|
106
|
+
if (typeof contract.evaluateCapability !== "function")
|
|
107
|
+
throw new Error(`CapabilityContract '${reducerId}' is missing evaluateCapability(manifest, requirement)`);
|
|
108
|
+
if (typeof contract.readManifest !== "function")
|
|
109
|
+
throw new Error(`CapabilityContract '${reducerId}' is missing readManifest(agentId)`);
|
|
110
|
+
if (typeof contract.requirements !== "function")
|
|
111
|
+
throw new Error(`CapabilityContract '${reducerId}' is missing requirements()`);
|
|
112
|
+
CAPABILITY_CONTRACTS.set(reducerId, contract);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveCapabilityContract(reducerId: string): CapabilityContract | undefined {
|
|
116
|
+
return CAPABILITY_CONTRACTS.get(reducerId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function listCapabilityContractIds(): string[] {
|
|
120
|
+
return [...CAPABILITY_CONTRACTS.keys()];
|
|
121
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// ZOB Harness — Agent capability launch-time gate primitive (WS-CH2 keystone,
|
|
2
|
+
// capability-validation PART II).
|
|
3
|
+
//
|
|
4
|
+
// The launch-time GATE on top of the WS-CH1 CapabilityContract: a pure primitive
|
|
5
|
+
// `runCapabilityGate(contract, agentIds, options)` that reads each manifest ONCE
|
|
6
|
+
// (via the contract's project-registered readManifest), evaluates every agent
|
|
7
|
+
// against its role's requirement (via the contract's evaluateCapability), and
|
|
8
|
+
// returns { ok, verdicts, fix_packet, shouldStart } — a pure verdict the app
|
|
9
|
+
// gates ZTEAM_LAUNCHER start on. It NEVER starts anything itself (mechanism in
|
|
10
|
+
// harness, action in app), preserving the shift-left split: the gate says
|
|
11
|
+
// "should this run start?" and the app acts on the answer. Mirrors the Round-4
|
|
12
|
+
// runLaunchGate invariant verbatim.
|
|
13
|
+
//
|
|
14
|
+
// THE HEADLINE DESIGN is fail-closed BY CONSTRUCTION — shouldStart === ok always,
|
|
15
|
+
// with NO parameter to opt out (the whole point of shifting agent-capability
|
|
16
|
+
// discovery from late runtime inside one agent — where it stalls ~2h and is
|
|
17
|
+
// nudge-spammed 59 times — to early launch when the operator who edits manifests
|
|
18
|
+
// is present). This is the temporal mirror of Round 4 applied to agent
|
|
19
|
+
// CONFIGURATION instead of environmental preconditions.
|
|
20
|
+
//
|
|
21
|
+
// IDEMPOTENT + RE-RUNNABLE: runCapabilityGate re-reads manifests (fresh
|
|
22
|
+
// contract.readManifest per agent) and re-evaluates on every call — it performs
|
|
23
|
+
// NO memoization. This is the property that makes "edit manifest → re-launch"
|
|
24
|
+
// work without a special path: after a manifest edit fixes a failing agent, a
|
|
25
|
+
// second runCapabilityGate call re-reads the now-fixed manifest and re-derives
|
|
26
|
+
// shouldStart === true.
|
|
27
|
+
//
|
|
28
|
+
// CRITICAL SAFETY DIVERGENCE from Round-4 EnvironmentContract (documented so it
|
|
29
|
+
// is never accidentally re-introduced): there is NO auto-resolve here. The gate
|
|
30
|
+
// EVALUATES + SURFACES a fix packet (buildFixPacket); APPLYING the manifest edit
|
|
31
|
+
// is operator-gated (a manifest edit changes an agent's authority =
|
|
32
|
+
// security-sensitive). grep this file for 'applyAutoResolve|autoFix|auto-resolve'
|
|
33
|
+
// returns ONLY this comment block — there is no such method. The Round-4
|
|
34
|
+
// environment launch-gate had an applyAutoResolve sibling (WS-PH3, allowlisted +
|
|
35
|
+
// REVERSIBLE env fixes); this capability gate intentionally does NOT, because
|
|
36
|
+
// editing a ZAgent manifest is a security-sensitive authority change that must
|
|
37
|
+
// stay operator-gated.
|
|
38
|
+
//
|
|
39
|
+
// Purity contract (the headline acceptance): this module performs NO direct
|
|
40
|
+
// filesystem access and NO process spawning. It imports type-only (erased at
|
|
41
|
+
// runtime) from the siblings ./types.js + ./capability-contract.js + a single
|
|
42
|
+
// runtime import of buildFixPacket (PURE) from ./primitives.js. The real IO
|
|
43
|
+
// (reading .pi/zagents/<id>.json into an AgentManifest) lives ENTIRELY in the
|
|
44
|
+
// project-registered readManifest (WS-CH3), never here. (The purity grep over
|
|
45
|
+
// this file — scanning for node:fs / node:child_process / spawnSync /
|
|
46
|
+
// readdirSync / readFileSync / exec( — returns NOTHING.)
|
|
47
|
+
//
|
|
48
|
+
// Metadata-only / body-free / network-disabled: LaunchGateResult /
|
|
49
|
+
// CapabilityFixPacketEntry carry agent ids, tool names, mode names, manifest
|
|
50
|
+
// paths, and fix_command strings only — no prompt bodies, no secrets, no
|
|
51
|
+
// allowedPaths contents.
|
|
52
|
+
import type { CapabilityContract } from "./capability-contract.js";
|
|
53
|
+
import type { AgentManifest, CapabilityVerdict } from "./types.js";
|
|
54
|
+
import { buildFixPacket, type CapabilityFixPacketEntry } from "./primitives.js";
|
|
55
|
+
|
|
56
|
+
// Re-export the fix-packet entry shape so consumers import it from the gate's
|
|
57
|
+
// surface (mirrors the WS-PH2 launch-gate re-exporting FixPacketEntry). The
|
|
58
|
+
// shape is defined in primitives.ts (buildFixPacket); the gate re-exports it so
|
|
59
|
+
// a downstream reader has one import site for the launch-gate result type.
|
|
60
|
+
export type { CapabilityFixPacketEntry } from "./primitives.js";
|
|
61
|
+
|
|
62
|
+
// --- Options ----------------------------------------------------------------
|
|
63
|
+
export interface LaunchGateOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Pluggable clock (deterministic in tests). The verdict itself is pure over
|
|
66
|
+
* (contract, manifests) and time-independent; this hook is reserved for an app
|
|
67
|
+
* to stamp emitted fix_packet entries. Reserved — not consumed by the core
|
|
68
|
+
* flow. (Mirrors the WS-PH2 runLaunchGate `now?` reserved hook.)
|
|
69
|
+
*
|
|
70
|
+
* NOTE: there is NO `manifests?` / `allowStart?` / `force?` option here. The
|
|
71
|
+
* manifests are read via the contract's readManifest (the only IO seam); and
|
|
72
|
+
* shouldStart === ok BY CONSTRUCTION with no opt-out — so no such option can
|
|
73
|
+
* exist. The only option is the reserved clock.
|
|
74
|
+
*/
|
|
75
|
+
readonly now?: () => Date;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Result -----------------------------------------------------------------
|
|
79
|
+
export interface LaunchGateResult {
|
|
80
|
+
/** True iff EVERY evaluated agent's verdict.ok === true (fail-closed). */
|
|
81
|
+
readonly ok: boolean;
|
|
82
|
+
/** One verdict per evaluated agent (agents with no role requirement are skipped). */
|
|
83
|
+
readonly verdicts: readonly CapabilityVerdict[];
|
|
84
|
+
/** One entry per FAILING agent, in evaluation order. Empty iff ok === true. */
|
|
85
|
+
readonly fix_packet: readonly CapabilityFixPacketEntry[];
|
|
86
|
+
/**
|
|
87
|
+
* === ok BY CONSTRUCTION (fail-closed). There is NO option to opt out of this:
|
|
88
|
+
* the gate refuses to start whenever any agent fails its capability contract.
|
|
89
|
+
* This is the shift-left guarantee — agent capability validated at launch, not
|
|
90
|
+
* ~2h into a run via nudge-spam.
|
|
91
|
+
*/
|
|
92
|
+
readonly shouldStart: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- The primitive ----------------------------------------------------------
|
|
96
|
+
/**
|
|
97
|
+
* Pure over (contract, agentIds). For each agentId: (1) read the manifest ONCE
|
|
98
|
+
* via contract.readManifest; (2) look up the manifest's role requirement via
|
|
99
|
+
* contract.requirements()[manifest.role] — agents with NO requirement are SKIPPED
|
|
100
|
+
* (permissive for unknown roles, not blocking; documented); (3) evaluate via
|
|
101
|
+
* contract.evaluateCapability(manifest, requirement) (the contract method, so the
|
|
102
|
+
* project can override — it delegates to compareCapability by default);
|
|
103
|
+
* (4) ok = every verdict.ok === true; (5) fix_packet from the FAILING verdicts via
|
|
104
|
+
* buildFixPacket (one entry per failure); (6) shouldStart === ok BY CONSTRUCTION
|
|
105
|
+
* (no opt-out — the headline fail-closed shift-left guarantee).
|
|
106
|
+
*
|
|
107
|
+
* SYNC when readManifest returns synchronously; returns a
|
|
108
|
+
* Promise<LaunchGateResult> ONLY when readManifest returns a Promise (each
|
|
109
|
+
* manifest is read once, then the set is shared across the evaluation). The union
|
|
110
|
+
* return type accommodates both paths.
|
|
111
|
+
*
|
|
112
|
+
* IDEMPOTENT + RE-RUNNABLE: no memoization. A second call after a manifest edit
|
|
113
|
+
* re-reads and re-evaluates, so "edit manifest → re-launch" works without a
|
|
114
|
+
* special path. NEVER starts anything itself — it is a pure verdict the app acts
|
|
115
|
+
* on (same split as Round 4's runLaunchGate).
|
|
116
|
+
*
|
|
117
|
+
* NO AUTO-RESOLVE: this function evaluates + surfaces a fix packet; it does NOT
|
|
118
|
+
* apply any manifest edit. Applying the edit is operator-gated (security-
|
|
119
|
+
* sensitive authority change).
|
|
120
|
+
*/
|
|
121
|
+
export function runCapabilityGate(
|
|
122
|
+
contract: CapabilityContract,
|
|
123
|
+
agentIds: readonly string[],
|
|
124
|
+
// `options` is reserved (the `now?` clock hook); the core flow is pure over
|
|
125
|
+
// (contract, agentIds) and does not consume it. Kept on the signature so the
|
|
126
|
+
// reserved hook is available to an app without a breaking change later.
|
|
127
|
+
options: LaunchGateOptions = {},
|
|
128
|
+
): LaunchGateResult | Promise<LaunchGateResult> {
|
|
129
|
+
// (1) Read each manifest ONCE via the contract's project-registered readManifest.
|
|
130
|
+
// The IO lives in the contract (app-side), NEVER here. readManifest may be
|
|
131
|
+
// sync OR async (a project may register an async reader); the declared
|
|
132
|
+
// WS-CH1 signature is sync, but we tolerate a Promise return at runtime so a
|
|
133
|
+
// project whose reader is async composes without a wrapper. Each manifest is
|
|
134
|
+
// read exactly once (no repeated IO per agent).
|
|
135
|
+
const manifestReads = agentIds.map(
|
|
136
|
+
(agentId) => contract.readManifest(agentId) as AgentManifest | Promise<AgentManifest>,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Build the verdict from resolved manifests. Pure over (contract, manifests).
|
|
140
|
+
const buildResult = (manifests: readonly AgentManifest[]): LaunchGateResult => {
|
|
141
|
+
const requirements = contract.requirements();
|
|
142
|
+
const verdicts: CapabilityVerdict[] = [];
|
|
143
|
+
const fix_packet: CapabilityFixPacketEntry[] = [];
|
|
144
|
+
|
|
145
|
+
for (const manifest of manifests) {
|
|
146
|
+
// (2) Look up the manifest's role requirement. Agents with no declared
|
|
147
|
+
// requirement are SKIPPED (permissive for unknown roles, not blocking).
|
|
148
|
+
// The launch gate evaluates only agents whose role has a protocol-declared
|
|
149
|
+
// tool contract; an unknown role is treated as "no requirement" rather
|
|
150
|
+
// than a blocker (the operator can add a requirement to enforce it).
|
|
151
|
+
const requirement = requirements[manifest.role];
|
|
152
|
+
if (!requirement) continue;
|
|
153
|
+
|
|
154
|
+
// (3) Evaluate via the contract method (delegates to compareCapability by
|
|
155
|
+
// default; the project can override). Single site of capability
|
|
156
|
+
// satisfaction per the WS-CH1 contract.
|
|
157
|
+
const verdict = contract.evaluateCapability(manifest, requirement);
|
|
158
|
+
verdicts.push(verdict);
|
|
159
|
+
|
|
160
|
+
// (5) fix_packet: one entry per FAILING agent, in evaluation order. Built
|
|
161
|
+
// from the verdict + the manifest + the requirement via buildFixPacket
|
|
162
|
+
// (the metadata-only remediation shape from primitives.ts).
|
|
163
|
+
if (!verdict.ok) {
|
|
164
|
+
fix_packet.push(buildFixPacket(verdict, manifest, requirement));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// (4) ok iff EVERY verdict.ok === true (fail-closed — any single failure
|
|
169
|
+
// blocks start). An empty verdict set (no agents had requirements) is ok.
|
|
170
|
+
const ok = verdicts.every((v) => v.ok === true);
|
|
171
|
+
|
|
172
|
+
// (6) shouldStart === ok BY CONSTRUCTION. No option opts out of fail-closed.
|
|
173
|
+
// This is the headline shift-left guarantee — derived from ok, never set
|
|
174
|
+
// independently.
|
|
175
|
+
return { ok, verdicts, fix_packet, shouldStart: ok };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// SYNC when every readManifest returns synchronously; ASYNC only when at least
|
|
179
|
+
// one readManifest returns a Promise (await them all via Promise.all, then
|
|
180
|
+
// build the result once over the resolved set).
|
|
181
|
+
if (manifestReads.some((read) => read instanceof Promise)) {
|
|
182
|
+
return Promise.all(manifestReads).then(buildResult);
|
|
183
|
+
}
|
|
184
|
+
return buildResult(manifestReads as readonly AgentManifest[]);
|
|
185
|
+
}
|