gentle-pi 2.3.0 → 2.5.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/README.md +195 -11
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/orchestrator-delegation.md +21 -10
- package/assets/orchestrator.md +8 -12
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/delegated-verification.md +25 -0
- package/docs/review-integration.md +1 -1
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +1421 -143
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agent-home.ts +8 -0
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +103 -12
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-host-relay.ts +146 -60
- package/lib/review-integration-v2.ts +92 -13
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +219 -0
- package/lib/sdd-preflight.ts +2 -2
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +102 -11
- package/runtime/review-integration-v2.mjs +92 -13
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +18 -13
- package/skills/_shared/review-ledger-contract.md +9 -1
- package/skills/issue-creation/SKILL.md +53 -93
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +31 -14
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +33 -1
- package/tests/native-review-cli.test.ts +40 -0
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +29 -22
- package/tests/orchestrator-budget.test.ts +71 -2
- package/tests/orchestrator-rdd-ownership.test.ts +10 -1
- package/tests/package-manifest.test.ts +134 -9
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +142 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-routing.test.ts +229 -11
- package/tests/review-host-relay.test.ts +195 -7
- package/tests/review-integration-v2-forward.test.ts +47 -0
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-ledger-contract.test.ts +1 -1
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +608 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +126 -0
- package/tests/runtime-harness.mjs +1 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
- package/tests/writer-edit-surface-scope.test.ts +153 -17
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MouseRegion,
|
|
3
|
+
type Component,
|
|
4
|
+
type TuiMouseEvent,
|
|
5
|
+
type TuiMouseEventResult,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
export type NativePointerCallback = (event: TuiMouseEvent) => TuiMouseEventResult | undefined;
|
|
9
|
+
|
|
10
|
+
export interface NativePointerRegionCallbacks {
|
|
11
|
+
onHover?: NativePointerCallback;
|
|
12
|
+
onLeave?: (event?: TuiMouseEvent) => void;
|
|
13
|
+
onPress?: NativePointerCallback;
|
|
14
|
+
onClick?: NativePointerCallback;
|
|
15
|
+
onWheel?: NativePointerCallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NativePointerMouseObserver {
|
|
19
|
+
beforeMouse(event: TuiMouseEvent): void;
|
|
20
|
+
afterMouse(event: TuiMouseEvent): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A composable adapter around the public MouseRegion component.
|
|
25
|
+
* Child dispatch stays first, so an already handled child result is unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export class NativePointerRegion implements Component {
|
|
28
|
+
private readonly adapter: MouseRegion;
|
|
29
|
+
private readonly callbacks: NativePointerRegionCallbacks;
|
|
30
|
+
private readonly scope: NativePointerScope | undefined;
|
|
31
|
+
private hovered = false;
|
|
32
|
+
private disabled = false;
|
|
33
|
+
private disposed = false;
|
|
34
|
+
private renderedWidth: number | undefined;
|
|
35
|
+
|
|
36
|
+
constructor(child: Component, callbacks: NativePointerRegionCallbacks, scope?: NativePointerScope) {
|
|
37
|
+
this.callbacks = callbacks;
|
|
38
|
+
this.scope = scope;
|
|
39
|
+
this.adapter = new MouseRegion(child, (event) => this.handlePointer(event));
|
|
40
|
+
this.scope?.register(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
render(width: number): string[] {
|
|
44
|
+
if (this.renderedWidth !== undefined && this.renderedWidth !== width) this.clearHover();
|
|
45
|
+
this.renderedWidth = width;
|
|
46
|
+
return this.adapter.render(width);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
50
|
+
if (this.disposed) return undefined;
|
|
51
|
+
return this.adapter.handleMouse(event);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
invalidate(): void {
|
|
55
|
+
this.renderedWidth = undefined;
|
|
56
|
+
this.clearHover();
|
|
57
|
+
this.adapter.invalidate();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setDisabled(disabled: boolean): void {
|
|
61
|
+
if (this.disabled === disabled) return;
|
|
62
|
+
this.disabled = disabled;
|
|
63
|
+
if (disabled) this.clearHover();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clearHover(event?: TuiMouseEvent): boolean {
|
|
67
|
+
if (!this.hovered) return false;
|
|
68
|
+
this.hovered = false;
|
|
69
|
+
this.callbacks.onLeave?.(event);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
dispose(): void {
|
|
74
|
+
if (this.disposed) return;
|
|
75
|
+
this.disposed = true;
|
|
76
|
+
this.clearHover();
|
|
77
|
+
this.scope?.unregister(this);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private handlePointer(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
81
|
+
if (this.disabled || this.disposed) return undefined;
|
|
82
|
+
if (event.type === "move" && event.button === "none") {
|
|
83
|
+
this.scope?.markMove(this, event);
|
|
84
|
+
this.hovered = true;
|
|
85
|
+
return this.callbacks.onHover?.(event);
|
|
86
|
+
}
|
|
87
|
+
if (event.type === "press") return this.callbacks.onPress?.(event);
|
|
88
|
+
if (event.type === "click") return this.callbacks.onClick?.(event);
|
|
89
|
+
if (event.type === "wheel") return this.callbacks.onWheel?.(event);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Coordinates pointer regions with a root-compatible native mouse observer. */
|
|
95
|
+
export class NativePointerScope {
|
|
96
|
+
private readonly regions = new Set<NativePointerRegion>();
|
|
97
|
+
private moved = false;
|
|
98
|
+
private disabled = false;
|
|
99
|
+
private disposed = false;
|
|
100
|
+
|
|
101
|
+
wrap(child: Component, callbacks: NativePointerRegionCallbacks): NativePointerRegion {
|
|
102
|
+
const region = new NativePointerRegion(child, callbacks, this);
|
|
103
|
+
region.setDisabled(this.disabled);
|
|
104
|
+
if (this.disposed) region.dispose();
|
|
105
|
+
return region;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
createMouseObserver(requestRender?: () => void): NativePointerMouseObserver {
|
|
109
|
+
return {
|
|
110
|
+
beforeMouse: (event) => {
|
|
111
|
+
if (!this.disposed && event.type === "move" && event.button === "none") this.moved = false;
|
|
112
|
+
},
|
|
113
|
+
afterMouse: (event) => {
|
|
114
|
+
if (!this.isInRoot(event) || this.moved) return;
|
|
115
|
+
if (this.clearHover(event)) requestRender?.();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setDisabled(disabled: boolean): void {
|
|
121
|
+
this.disabled = disabled;
|
|
122
|
+
for (const region of this.regions) region.setDisabled(disabled);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
invalidate(): void {
|
|
126
|
+
this.clearHover();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dispose(): void {
|
|
130
|
+
if (this.disposed) return;
|
|
131
|
+
this.disposed = true;
|
|
132
|
+
for (const region of [...this.regions]) region.dispose();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
register(region: NativePointerRegion): void {
|
|
136
|
+
if (!this.disposed) this.regions.add(region);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
unregister(region: NativePointerRegion): void {
|
|
140
|
+
this.regions.delete(region);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
markMove(region: NativePointerRegion, event: TuiMouseEvent): void {
|
|
144
|
+
this.moved = true;
|
|
145
|
+
for (const candidate of this.regions) {
|
|
146
|
+
if (candidate !== region) candidate.clearHover(event);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private clearHover(event?: TuiMouseEvent): boolean {
|
|
151
|
+
let changed = false;
|
|
152
|
+
for (const region of this.regions) changed = region.clearHover(event) || changed;
|
|
153
|
+
return changed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private isInRoot(event: TuiMouseEvent): boolean {
|
|
157
|
+
return !this.disposed && event.type === "move" && event.button === "none" &&
|
|
158
|
+
event.x >= 0 && event.x < event.width && event.y >= 0 && event.y < event.height;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createNativePointerScope(): NativePointerScope {
|
|
163
|
+
return new NativePointerScope();
|
|
164
|
+
}
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, join, posix, win32 } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
7
7
|
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
|
|
8
|
+
import { decodeReviewAssessmentV1, type ReviewAssessmentV1 } from "./review-risk-assessment.ts";
|
|
8
9
|
import {
|
|
9
10
|
REVIEW_INTEGRATION_CONTRACT,
|
|
10
11
|
decodeReviewAcknowledgedV1,
|
|
@@ -59,6 +60,7 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
59
60
|
RECONCILE_AUTHORITY: "review/reconcile-authority",
|
|
60
61
|
REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
|
|
61
62
|
MODE: "review/mode",
|
|
63
|
+
ASSESS: "review/assess",
|
|
62
64
|
REPAIR: "review/repair",
|
|
63
65
|
CAPTURE_RESULT: "review/capture-result",
|
|
64
66
|
CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
|
|
@@ -109,6 +111,11 @@ export interface NativeReviewCli {
|
|
|
109
111
|
// outside the negotiated review-integration protocol — same shape as
|
|
110
112
|
// reviewStatus/reclaim above.
|
|
111
113
|
reviewMode?(request: NativeReviewModeRequest): Promise<NativeReviewModeResult>;
|
|
114
|
+
// Read-only risk assessment (gentle-ai#4295, landing in parallel with
|
|
115
|
+
// gentle-pi#662). Same plain-versioned shape as reviewMode/reviewStatus:
|
|
116
|
+
// an older binary without the verb, or any other process/decode failure,
|
|
117
|
+
// rejects the returned promise -- callers fail closed to `high` risk.
|
|
118
|
+
assess?(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1>;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
export const NATIVE_REVIEW_MODE_OPERATION = {
|
|
@@ -151,6 +158,17 @@ export interface NativeReviewModeRequest {
|
|
|
151
158
|
signal?: AbortSignal;
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
// Read-only risk assessment request (gentle-pi#662). `baseRef` requires
|
|
162
|
+
// explicit `committedOnly` acknowledgement, exactly like Native START's
|
|
163
|
+
// baseRef/committedOnly pairing, because both select a committed range
|
|
164
|
+
// instead of the ambient working tree.
|
|
165
|
+
export interface NativeReviewAssessRequest {
|
|
166
|
+
cwd: string;
|
|
167
|
+
baseRef?: string;
|
|
168
|
+
committedOnly?: boolean;
|
|
169
|
+
signal?: AbortSignal;
|
|
170
|
+
}
|
|
171
|
+
|
|
154
172
|
export interface NativeReviewModeStatus {
|
|
155
173
|
global: NativeReviewModeValue;
|
|
156
174
|
cloneLocal: NativeReviewModeValue;
|
|
@@ -261,7 +279,6 @@ export interface NativeReviewAbandonRequest {
|
|
|
261
279
|
snapshotIdentity: string;
|
|
262
280
|
capturedLensResults: readonly string[];
|
|
263
281
|
findingsPresent: boolean;
|
|
264
|
-
evidenceRecordsPresent: boolean;
|
|
265
282
|
actor: string;
|
|
266
283
|
reason: string;
|
|
267
284
|
maintainerAuthorization: string;
|
|
@@ -498,8 +515,13 @@ export const NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION = {
|
|
|
498
515
|
} as const;
|
|
499
516
|
export type NativeReviewAuthorityEntryVersion = (typeof NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)[keyof typeof NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION];
|
|
500
517
|
|
|
501
|
-
export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS =
|
|
502
|
-
|
|
518
|
+
export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS = {
|
|
519
|
+
...NATIVE_REVIEW_AUTHORITY_STATUS,
|
|
520
|
+
INCOMPLETE_STORE_ENTRY: "incomplete-store-entry",
|
|
521
|
+
HISTORICAL_PRE_RECEIPT: "historical-pre-receipt",
|
|
522
|
+
INVALIDATED: "invalidated",
|
|
523
|
+
} as const;
|
|
524
|
+
export type NativeReviewAuthorityEntryStatus = (typeof NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS)[keyof typeof NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS];
|
|
503
525
|
|
|
504
526
|
export const NATIVE_REVIEW_LOCK_STATUS = {
|
|
505
527
|
OWNED: "owned",
|
|
@@ -541,6 +563,10 @@ export interface NativeReviewRecovery {
|
|
|
541
563
|
recoveredAt: string;
|
|
542
564
|
maintainerAuthorization?: string;
|
|
543
565
|
}
|
|
566
|
+
export interface NativeReviewDiscardedWorkSummary {
|
|
567
|
+
capturedLensResults: readonly string[];
|
|
568
|
+
findingsPresent: boolean;
|
|
569
|
+
}
|
|
544
570
|
export interface NativeReviewAuthorityEntry {
|
|
545
571
|
version: NativeReviewAuthorityEntryVersion;
|
|
546
572
|
lineageId?: string;
|
|
@@ -551,6 +577,7 @@ export interface NativeReviewAuthorityEntry {
|
|
|
551
577
|
snapshotIdentity?: string;
|
|
552
578
|
chainIdentity?: string;
|
|
553
579
|
recovery?: NativeReviewRecovery;
|
|
580
|
+
discardedWork?: NativeReviewDiscardedWorkSummary;
|
|
554
581
|
problems: readonly string[];
|
|
555
582
|
}
|
|
556
583
|
export interface NativeReviewAuthorityLock {
|
|
@@ -789,6 +816,33 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
789
816
|
// the columns match the 2.5.0-rc.3 row. riskEvidence and hint stay dark:
|
|
790
817
|
// still not proven to reach the negotiated START path Pi reads.
|
|
791
818
|
"2.5.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
819
|
+
// Ground-truthed against the published v2.6.0 binary installed from its
|
|
820
|
+
// signed archive: `review capabilities` on the v2 lane advertises
|
|
821
|
+
// capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
|
|
822
|
+
// and the `start/v4` continuation, all of which the decoders already read.
|
|
823
|
+
// `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
|
|
824
|
+
// and `review validate --gate pre-commit` still answers
|
|
825
|
+
// `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
|
|
826
|
+
// against the binary. Declining the consent/v3 prompt during `review start`
|
|
827
|
+
// still returns `risk_evidence` only on that blocking envelope, never on
|
|
828
|
+
// the negotiated `start/v4` continuation, so riskEvidence and hint stay
|
|
829
|
+
// dark for the same reason they have on every row since 2.2.0: still not
|
|
830
|
+
// proven to reach the negotiated START path Pi reads. The closed fields Pi
|
|
831
|
+
// consumes did not change between 2.5.0 and 2.6.0, so the columns match
|
|
832
|
+
// the 2.5.0 row.
|
|
833
|
+
"2.6.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
834
|
+
// Ground-truthed against the published v2.7.0 binary installed from its
|
|
835
|
+
// signed archive: `review capabilities` on the v2 lane still advertises
|
|
836
|
+
// capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
|
|
837
|
+
// and the `start/v4` continuation, all of which the decoders already read.
|
|
838
|
+
// `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
|
|
839
|
+
// and `review validate --gate pre-commit` still answers
|
|
840
|
+
// `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
|
|
841
|
+
// against the binary. riskEvidence and hint stay dark for the same reason
|
|
842
|
+
// they have on every row since 2.2.0: still not proven to reach the
|
|
843
|
+
// negotiated START path Pi reads. The closed fields Pi consumes did not
|
|
844
|
+
// change between 2.6.0 and 2.7.0, so the columns match the 2.6.0 row.
|
|
845
|
+
"2.7.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
792
846
|
});
|
|
793
847
|
|
|
794
848
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -1031,8 +1085,15 @@ function decodeNativeReviewRecovery(value: unknown): NativeReviewRecovery {
|
|
|
1031
1085
|
...(recovery.maintainer_authorization === undefined ? {} : { maintainerAuthorization: requiredString(recovery.maintainer_authorization) }),
|
|
1032
1086
|
};
|
|
1033
1087
|
}
|
|
1088
|
+
function decodeNativeReviewDiscardedWorkSummary(value: unknown): NativeReviewDiscardedWorkSummary {
|
|
1089
|
+
const discardedWork = exactObject(value, ["captured_lens_results", "findings_present"]);
|
|
1090
|
+
return {
|
|
1091
|
+
capturedLensResults: stringArray(discardedWork.captured_lens_results),
|
|
1092
|
+
findingsPresent: booleanValue(discardedWork.findings_present),
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1034
1095
|
function decodeNativeReviewStatusEntry(value: unknown): NativeReviewAuthorityEntry {
|
|
1035
|
-
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery"]);
|
|
1096
|
+
const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery", "discarded_work"]);
|
|
1036
1097
|
return {
|
|
1037
1098
|
version: enumString(entry.version, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)) as NativeReviewAuthorityEntryVersion,
|
|
1038
1099
|
...(entry.lineage_id === undefined ? {} : { lineageId: requiredString(entry.lineage_id) }),
|
|
@@ -1043,6 +1104,7 @@ function decodeNativeReviewStatusEntry(value: unknown): NativeReviewAuthorityEnt
|
|
|
1043
1104
|
...(entry.snapshot_identity === undefined ? {} : { snapshotIdentity: sha256Identity(entry.snapshot_identity) }),
|
|
1044
1105
|
...(entry.chain_identity === undefined ? {} : { chainIdentity: requiredString(entry.chain_identity) }),
|
|
1045
1106
|
...(entry.recovery === undefined ? {} : { recovery: decodeNativeReviewRecovery(entry.recovery) }),
|
|
1107
|
+
...(entry.discarded_work === undefined ? {} : { discardedWork: decodeNativeReviewDiscardedWorkSummary(entry.discarded_work) }),
|
|
1046
1108
|
problems: stringArray(entry.problems),
|
|
1047
1109
|
};
|
|
1048
1110
|
}
|
|
@@ -1357,9 +1419,7 @@ class NativeReviewPlainCli {
|
|
|
1357
1419
|
if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
|
|
1358
1420
|
}
|
|
1359
1421
|
if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
|
|
1360
|
-
|
|
1361
|
-
if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
|
|
1362
|
-
}
|
|
1422
|
+
if (typeof request.findingsPresent !== "boolean") throw new TypeError("Native ABANDON findingsPresent must be a boolean");
|
|
1363
1423
|
if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
|
|
1364
1424
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
|
|
1365
1425
|
"review", "abandon", "--cwd", request.cwd,
|
|
@@ -1448,7 +1508,11 @@ class NativeReviewPlainCli {
|
|
|
1448
1508
|
}
|
|
1449
1509
|
}
|
|
1450
1510
|
|
|
1451
|
-
export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAbandonRequest, "lineage" | "expectedRevision" | "snapshotIdentity" | "capturedLensResults" | "findingsPresent" | "
|
|
1511
|
+
export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAbandonRequest, "lineage" | "expectedRevision" | "snapshotIdentity" | "capturedLensResults" | "findingsPresent" | "actor" | "reason">): string {
|
|
1512
|
+
// gentle-ai 0ed9225f removed evidence records from the discarded-work summary,
|
|
1513
|
+
// so the native v2 gate verifies an exact eight-line binding (schema, lineage,
|
|
1514
|
+
// revision, snapshot_identity, reason, captured_lens_results, findings_present,
|
|
1515
|
+
// actor) — there is no evidence_records_present line to derive or relay.
|
|
1452
1516
|
return [
|
|
1453
1517
|
"gentle-ai.review-abandon-authorization/v2",
|
|
1454
1518
|
`lineage=${request.lineage}`,
|
|
@@ -1457,7 +1521,6 @@ export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAband
|
|
|
1457
1521
|
`reason=${request.reason}`,
|
|
1458
1522
|
`captured_lens_results=${request.capturedLensResults.join(",")}`,
|
|
1459
1523
|
`findings_present=${request.findingsPresent}`,
|
|
1460
|
-
`evidence_records_present=${request.evidenceRecordsPresent}`,
|
|
1461
1524
|
`actor=${request.actor.trim()}`,
|
|
1462
1525
|
].join("\n");
|
|
1463
1526
|
}
|
|
@@ -1580,14 +1643,20 @@ function splitNativeConsentInvocation(invocation: string): readonly string[] {
|
|
|
1580
1643
|
let quote: "'" | '"' | undefined;
|
|
1581
1644
|
let escaping = false;
|
|
1582
1645
|
let started = false;
|
|
1583
|
-
|
|
1646
|
+
const source = invocation.trim();
|
|
1647
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1648
|
+
const character = source[index]!;
|
|
1584
1649
|
if (escaping) {
|
|
1585
1650
|
current += character;
|
|
1586
1651
|
escaping = false;
|
|
1587
1652
|
started = true;
|
|
1588
1653
|
continue;
|
|
1589
1654
|
}
|
|
1590
|
-
|
|
1655
|
+
// Provider invocations are not shell commands. Keep every path backslash
|
|
1656
|
+
// verbatim, including quoted UNC and drive-root paths. Outside quotes,
|
|
1657
|
+
// only a backslash before whitespace joins a space-containing token.
|
|
1658
|
+
const next = source[index + 1];
|
|
1659
|
+
if (character === "\\" && quote === undefined && next !== undefined && /\s/.test(next)) {
|
|
1591
1660
|
escaping = true;
|
|
1592
1661
|
started = true;
|
|
1593
1662
|
continue;
|
|
@@ -1678,7 +1747,9 @@ function consentInvocationArguments(request: NativeReviewConsentAnswerRequest):
|
|
|
1678
1747
|
if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new NativeReviewConsentBindingError("consent-invocation-not-start", "Native consent invocation is not a provider review START");
|
|
1679
1748
|
const arguments_ = words.slice(1);
|
|
1680
1749
|
if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new NativeReviewConsentBindingError("consent-invocation-contract-changed", "Native consent invocation contract changed");
|
|
1681
|
-
|
|
1750
|
+
const providerCwd = exactConsentOption(arguments_, "--cwd");
|
|
1751
|
+
const requestedCwd = normalizeNativeReviewCwd(request.cwd);
|
|
1752
|
+
if (normalizeNativeReviewCwd(providerCwd) !== requestedCwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
|
|
1682
1753
|
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
|
|
1683
1754
|
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
|
|
1684
1755
|
const lineageId = optionalConsentLineageOption(arguments_);
|
|
@@ -2178,6 +2249,26 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2178
2249
|
return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
|
|
2179
2250
|
}
|
|
2180
2251
|
|
|
2252
|
+
// Read-only risk assessment (gentle-ai#4295, gentle-pi#662). Never mutates;
|
|
2253
|
+
// an older binary without the verb, or any other process/decode failure,
|
|
2254
|
+
// rejects -- callers (the `gentle_review` tool's `assess` operation) fail
|
|
2255
|
+
// closed to `high`.
|
|
2256
|
+
async assess(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1> {
|
|
2257
|
+
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native ASSESS baseRef must be a non-empty, trimmed, NUL-free string");
|
|
2258
|
+
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native ASSESS baseRef requires explicit committedOnly acknowledgement");
|
|
2259
|
+
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native ASSESS committedOnly requires an explicit baseRef");
|
|
2260
|
+
const cwd = await canonicalNativeReviewCwd(request.cwd);
|
|
2261
|
+
const execution = await this.invoke(
|
|
2262
|
+
NATIVE_REVIEW_OPERATION.ASSESS,
|
|
2263
|
+
cwd,
|
|
2264
|
+
["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), "--json"],
|
|
2265
|
+
false,
|
|
2266
|
+
request.signal,
|
|
2267
|
+
this.executablePath(NATIVE_REVIEW_OPERATION.ASSESS, false),
|
|
2268
|
+
);
|
|
2269
|
+
return decode(NATIVE_REVIEW_OPERATION.ASSESS, false, () => decodeReviewAssessmentV1(execution.body));
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2181
2272
|
// Recovery commands are version-gated plain CLI operations outside the
|
|
2182
2273
|
// negotiated integration-v1 contract, exactly like reviewStatus.
|
|
2183
2274
|
reclaim(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult> {
|
|
@@ -31,6 +31,8 @@ export const PI_RUNTIME_IDENTITY = "pi";
|
|
|
31
31
|
export const SUPPORTED_PROVIDER_CONTRACT_MAJOR = 1;
|
|
32
32
|
// `runtimes` became part of the manifest in contract 1.1.0.
|
|
33
33
|
export const RUNTIMES_REQUIRED_SINCE_MINOR = 1;
|
|
34
|
+
// `orchestration` became part of the manifest in contract 1.2.0.
|
|
35
|
+
export const ORCHESTRATION_REQUIRED_SINCE_MINOR = 2;
|
|
34
36
|
// Exactly the provider roles gentle-pi supports, in the manifest's strict order.
|
|
35
37
|
export const PROVIDER_CONTRACT_ROLE_IDS = ["lens", "refuter", "targeted-validator"] as const;
|
|
36
38
|
// Every mandatory capability a role may declare. An unknown mandatory
|
|
@@ -42,6 +44,13 @@ const MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
|
42
44
|
const MAX_BUNDLE_BYTES = 16 * 1024 * 1024;
|
|
43
45
|
const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
44
46
|
const BUNDLE_ENTRY_COUNT = 8;
|
|
47
|
+
// Generous fixed safety bound on raw archive/tree entries read BEFORE the
|
|
48
|
+
// manifest itself is parsed (the exact expected count, base entries plus one
|
|
49
|
+
// per `orchestration` runtime, is known only after manifest decode). Real
|
|
50
|
+
// contracts register a handful of runtimes; this bound only rules out an
|
|
51
|
+
// absurd smuggled inventory, it is not the exact-count check.
|
|
52
|
+
const MAX_ORCHESTRATION_ENTRIES = 64;
|
|
53
|
+
const MAX_RAW_BUNDLE_ENTRIES = BUNDLE_ENTRY_COUNT + MAX_ORCHESTRATION_ENTRIES;
|
|
45
54
|
// setuid/setgid/sticky, any execute bit, or world-writable content is never
|
|
46
55
|
// acceptable for a data-only contract bundle.
|
|
47
56
|
const FORBIDDEN_MODE_BITS = 0o7113;
|
|
@@ -84,7 +93,13 @@ export interface VerifiedProviderContractBundle {
|
|
|
84
93
|
/** Present exactly when the manifest declares the runtime registry (>= 1.1.0). */
|
|
85
94
|
readonly runtimes: readonly string[] | undefined;
|
|
86
95
|
readonly roles: readonly VerifiedProviderRole[];
|
|
87
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Verified runtime-bound review execution contract text, runtime identity
|
|
98
|
+
* -> UTF-8 `orchestration/<runtime>.md` contents. Empty before contract
|
|
99
|
+
* 1.2.0, where `orchestration` does not exist in the manifest.
|
|
100
|
+
*/
|
|
101
|
+
readonly orchestration: ReadonlyMap<string, string>;
|
|
102
|
+
/** Canonical path -> exact verified bytes for all entries (base eight plus one per orchestration runtime). */
|
|
88
103
|
readonly entries: ReadonlyMap<string, Buffer>;
|
|
89
104
|
/** Canonical path -> SHA-256 of the exact verified bytes. */
|
|
90
105
|
readonly entrySha256: ReadonlyMap<string, string>;
|
|
@@ -306,6 +321,23 @@ function parseManifestRole(value: unknown, index: number): ManifestRole {
|
|
|
306
321
|
};
|
|
307
322
|
}
|
|
308
323
|
|
|
324
|
+
interface ManifestOrchestrationEntry {
|
|
325
|
+
readonly runtime: string;
|
|
326
|
+
readonly file: ManifestFileReference;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function parseOrchestrationEntry(value: unknown, index: number): ManifestOrchestrationEntry {
|
|
330
|
+
const where = `manifest.orchestration[${index}]`;
|
|
331
|
+
const object = requireObject(value, where);
|
|
332
|
+
requireExactKeys(object, ["runtime", "file"], ["runtime", "file"], where);
|
|
333
|
+
const runtime = requireString(object.runtime, `${where}.runtime`);
|
|
334
|
+
if (!RUNTIME_IDENTITY_PATTERN.test(runtime)) invalid(`${where}.runtime ${JSON.stringify(runtime)} is not a runtime identity`);
|
|
335
|
+
const file = parseFileReference(object.file, `${where}.file`);
|
|
336
|
+
const expectedPath = `orchestration/${runtime}.md`;
|
|
337
|
+
if (file.path !== expectedPath) invalid(`${where}.file.path must be ${JSON.stringify(expectedPath)}`);
|
|
338
|
+
return { runtime, file };
|
|
339
|
+
}
|
|
340
|
+
|
|
309
341
|
interface ParsedManifest {
|
|
310
342
|
readonly contractSemver: string;
|
|
311
343
|
readonly major: number;
|
|
@@ -315,13 +347,14 @@ interface ParsedManifest {
|
|
|
315
347
|
readonly runtimes: readonly string[] | undefined;
|
|
316
348
|
readonly readme: ManifestFileReference;
|
|
317
349
|
readonly roles: readonly ManifestRole[];
|
|
350
|
+
readonly orchestration: readonly ManifestOrchestrationEntry[];
|
|
318
351
|
}
|
|
319
352
|
|
|
320
353
|
function parseManifest(payload: Buffer): ParsedManifest {
|
|
321
354
|
const object = decodeStrictManifestObject(payload);
|
|
322
355
|
requireExactKeys(
|
|
323
356
|
object,
|
|
324
|
-
["schema", "contract_semver", "transport_capability", "runtimes", "readme", "roles"],
|
|
357
|
+
["schema", "contract_semver", "transport_capability", "runtimes", "readme", "roles", "orchestration"],
|
|
325
358
|
["schema", "contract_semver", "transport_capability", "readme", "roles"],
|
|
326
359
|
"manifest",
|
|
327
360
|
);
|
|
@@ -365,6 +398,33 @@ function parseManifest(payload: Buffer): ParsedManifest {
|
|
|
365
398
|
invalid(`manifest.runtimes does not register the ${JSON.stringify(PI_RUNTIME_IDENTITY)} runtime identity`);
|
|
366
399
|
}
|
|
367
400
|
|
|
401
|
+
const orchestrationRequired = minor >= ORCHESTRATION_REQUIRED_SINCE_MINOR;
|
|
402
|
+
let orchestration: readonly ManifestOrchestrationEntry[] = [];
|
|
403
|
+
if (Object.prototype.hasOwnProperty.call(object, "orchestration")) {
|
|
404
|
+
if (!orchestrationRequired) {
|
|
405
|
+
invalid(`manifest.orchestration is not part of contract ${contractSemver}; it exists from 1.${ORCHESTRATION_REQUIRED_SINCE_MINOR}.0`);
|
|
406
|
+
}
|
|
407
|
+
if (!Array.isArray(object.orchestration) || object.orchestration.length === 0) {
|
|
408
|
+
invalid("manifest.orchestration must be a non-empty array");
|
|
409
|
+
}
|
|
410
|
+
const parsedOrchestration = object.orchestration.map((entry, index) => parseOrchestrationEntry(entry, index));
|
|
411
|
+
for (let position = 0; position < parsedOrchestration.length; position += 1) {
|
|
412
|
+
const entry = parsedOrchestration[position] as ManifestOrchestrationEntry;
|
|
413
|
+
if (position > 0 && (parsedOrchestration[position - 1] as ManifestOrchestrationEntry).runtime >= entry.runtime) {
|
|
414
|
+
invalid("manifest.orchestration must be strictly sorted by runtime and unique");
|
|
415
|
+
}
|
|
416
|
+
if (runtimes === undefined || !runtimes.includes(entry.runtime)) {
|
|
417
|
+
invalid(`manifest.orchestration[${position}].runtime ${JSON.stringify(entry.runtime)} is not registered in manifest.runtimes`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!parsedOrchestration.some((entry) => entry.runtime === PI_RUNTIME_IDENTITY)) {
|
|
421
|
+
invalid(`manifest.orchestration does not register the ${JSON.stringify(PI_RUNTIME_IDENTITY)} runtime identity`);
|
|
422
|
+
}
|
|
423
|
+
orchestration = parsedOrchestration;
|
|
424
|
+
} else if (orchestrationRequired) {
|
|
425
|
+
invalid(`manifest.orchestration is required from contract 1.${ORCHESTRATION_REQUIRED_SINCE_MINOR}.0 (got ${contractSemver} without it)`);
|
|
426
|
+
}
|
|
427
|
+
|
|
368
428
|
if (!Array.isArray(object.roles)) invalid("manifest.roles must be an array");
|
|
369
429
|
const roles = object.roles.map((role, index) => parseManifestRole(role, index));
|
|
370
430
|
if (roles.length !== PROVIDER_CONTRACT_ROLE_IDS.length) {
|
|
@@ -385,6 +445,7 @@ function parseManifest(payload: Buffer): ParsedManifest {
|
|
|
385
445
|
runtimes,
|
|
386
446
|
readme: parseFileReference(object.readme, "manifest.readme"),
|
|
387
447
|
roles,
|
|
448
|
+
orchestration,
|
|
388
449
|
};
|
|
389
450
|
}
|
|
390
451
|
|
|
@@ -467,8 +528,8 @@ export function bundleTreeSha256(entrySha256: ReadonlyMap<string, string>): stri
|
|
|
467
528
|
|
|
468
529
|
/** Verifies a complete in-memory bundle inventory. The single trust anchor. */
|
|
469
530
|
export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<string, Buffer>): VerifiedProviderContractBundle {
|
|
470
|
-
if (rawEntries.size
|
|
471
|
-
invalid(`bundle inventory has ${rawEntries.size} files;
|
|
531
|
+
if (rawEntries.size > MAX_RAW_BUNDLE_ENTRIES) {
|
|
532
|
+
invalid(`bundle inventory has ${rawEntries.size} files; at most ${MAX_RAW_BUNDLE_ENTRIES} are ever expected`);
|
|
472
533
|
}
|
|
473
534
|
for (const [name, payload] of rawEntries) {
|
|
474
535
|
if (!isCanonicalBundlePath(name)) invalid(`bundle path ${JSON.stringify(name)} is unsafe`);
|
|
@@ -478,6 +539,14 @@ export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<stri
|
|
|
478
539
|
if (manifestPayload === undefined) invalid("bundle is missing manifest.json");
|
|
479
540
|
const manifest = parseManifest(manifestPayload);
|
|
480
541
|
|
|
542
|
+
// The exact expected inventory size is only known once the manifest is
|
|
543
|
+
// decoded: base entries plus exactly one `orchestration/<runtime>.md` per
|
|
544
|
+
// registered orchestration runtime.
|
|
545
|
+
const expectedEntryCount = BUNDLE_ENTRY_COUNT + manifest.orchestration.length;
|
|
546
|
+
if (rawEntries.size !== expectedEntryCount) {
|
|
547
|
+
invalid(`bundle inventory has ${rawEntries.size} files; exactly ${expectedEntryCount} are expected`);
|
|
548
|
+
}
|
|
549
|
+
|
|
481
550
|
verifyFileReference(rawEntries, manifest.readme, "README.md");
|
|
482
551
|
const expected = new Set(["README.md", "manifest.json"]);
|
|
483
552
|
for (const role of manifest.roles) {
|
|
@@ -485,6 +554,13 @@ export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<stri
|
|
|
485
554
|
expected.add(`schemas/${role.id}.schema.json`);
|
|
486
555
|
expected.add(`vectors/${role.id}.json`);
|
|
487
556
|
}
|
|
557
|
+
const orchestration = new Map<string, string>();
|
|
558
|
+
for (const entry of manifest.orchestration) {
|
|
559
|
+
const path = `orchestration/${entry.runtime}.md`;
|
|
560
|
+
const payload = verifyFileReference(rawEntries, entry.file, path);
|
|
561
|
+
orchestration.set(entry.runtime, payload.toString("utf8"));
|
|
562
|
+
expected.add(path);
|
|
563
|
+
}
|
|
488
564
|
for (const name of rawEntries.keys()) {
|
|
489
565
|
if (!expected.has(name)) invalid(`bundle has unexpected file ${JSON.stringify(name)}`);
|
|
490
566
|
}
|
|
@@ -514,6 +590,7 @@ export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<stri
|
|
|
514
590
|
vectorPath: role.vector.path,
|
|
515
591
|
vectorSha256: role.vector.sha256,
|
|
516
592
|
})),
|
|
593
|
+
orchestration,
|
|
517
594
|
entries,
|
|
518
595
|
entrySha256,
|
|
519
596
|
treeSha256: bundleTreeSha256(entrySha256),
|
|
@@ -566,7 +643,7 @@ export function verifyProviderContractBundleTree(directory: string): VerifiedPro
|
|
|
566
643
|
}
|
|
567
644
|
if (!isCanonicalBundlePath(relativeName)) invalid(`bundle tree path ${JSON.stringify(relativeName)} is unsafe`);
|
|
568
645
|
if (details.size > MAX_FILE_BYTES) invalid(`bundle tree entry ${JSON.stringify(relativeName)} exceeds its size limit`);
|
|
569
|
-
if (entries.size >=
|
|
646
|
+
if (entries.size >= MAX_RAW_BUNDLE_ENTRIES) invalid(`bundle tree has more than ${MAX_RAW_BUNDLE_ENTRIES} files`);
|
|
570
647
|
entries.set(relativeName, readFileSync(absolute));
|
|
571
648
|
}
|
|
572
649
|
};
|
|
@@ -628,7 +705,7 @@ function readTarEntries(expanded: Buffer): Map<string, Buffer> {
|
|
|
628
705
|
if (size > MAX_FILE_BYTES) invalid(`tar entry ${JSON.stringify(name)} exceeds its size limit`);
|
|
629
706
|
totalBytes += size;
|
|
630
707
|
if (totalBytes > MAX_BUNDLE_BYTES) invalid("tar entries exceed the bundle size limit");
|
|
631
|
-
if (entries.size >=
|
|
708
|
+
if (entries.size >= MAX_RAW_BUNDLE_ENTRIES) invalid(`tar has more than ${MAX_RAW_BUNDLE_ENTRIES} entries`);
|
|
632
709
|
const padding = (TAR_BLOCK_SIZE - (size % TAR_BLOCK_SIZE)) % TAR_BLOCK_SIZE;
|
|
633
710
|
if (expanded.length - offset < size + padding) invalid("tar stream ends before a complete entry");
|
|
634
711
|
entries.set(name, Buffer.from(expanded.subarray(offset, offset + size)));
|
|
@@ -696,6 +773,11 @@ export function generateProviderContractBaselines(bundle: VerifiedProviderContra
|
|
|
696
773
|
mandatory_capabilities: mandatory,
|
|
697
774
|
runtimes: bundle.runtimes === undefined ? null : [...bundle.runtimes],
|
|
698
775
|
pi_registered: piRuntimeRegistration(bundle).registered,
|
|
776
|
+
orchestration: [...bundle.orchestration.keys()].sort().map((runtime) => ({
|
|
777
|
+
runtime,
|
|
778
|
+
path: `orchestration/${runtime}.md`,
|
|
779
|
+
sha256: bundle.entrySha256.get(`orchestration/${runtime}.md`),
|
|
780
|
+
})),
|
|
699
781
|
};
|
|
700
782
|
return new Map([
|
|
701
783
|
[PROVIDER_ROLES_BASELINE_FILE, `${JSON.stringify(roles, null, 2)}\n`],
|