gentle-pi 4.1.0 → 4.3.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 +352 -128
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +9 -9
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +35 -7
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +443 -8
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-shell-bin.test.ts +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +131 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +69 -9
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/vim-editor-adapter.test.ts +41 -15
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +24 -1
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// release bundle (or its extracted tree) and verifies it fail-closed before
|
|
5
5
|
// any layout, role, schema, or capability inside it is trusted:
|
|
6
6
|
//
|
|
7
|
-
// - exact eight
|
|
7
|
+
// - exact inventory: eight base entries (README.md, manifest.json, 3 schemas,
|
|
8
|
+
// 3 vectors) plus one `orchestration/<runtime>.md` (>= 1.2.0) and one
|
|
9
|
+
// `routing/<runtime>.md` (>= 1.4.0) per runtime the manifest lists;
|
|
8
10
|
// - strict manifest decode: single JSON object, no duplicate keys, no unknown fields;
|
|
9
11
|
// - per-file SHA-256 agreement between the manifest and the actual bytes;
|
|
10
12
|
// - pinned transport capability and supported contract major (1);
|
|
@@ -33,6 +35,8 @@ export const SUPPORTED_PROVIDER_CONTRACT_MAJOR = 1;
|
|
|
33
35
|
export const RUNTIMES_REQUIRED_SINCE_MINOR = 1;
|
|
34
36
|
// `orchestration` became part of the manifest in contract 1.2.0.
|
|
35
37
|
export const ORCHESTRATION_REQUIRED_SINCE_MINOR = 2;
|
|
38
|
+
// `routing` became part of the manifest in contract 1.4.0.
|
|
39
|
+
export const ROUTING_REQUIRED_SINCE_MINOR = 4;
|
|
36
40
|
// Exactly the provider roles gentle-pi supports, in the manifest's strict order.
|
|
37
41
|
export const PROVIDER_CONTRACT_ROLE_IDS = ["lens", "refuter", "targeted-validator"] as const;
|
|
38
42
|
// Every mandatory capability a role may declare. An unknown mandatory
|
|
@@ -46,11 +50,11 @@ const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
|
46
50
|
const BUNDLE_ENTRY_COUNT = 8;
|
|
47
51
|
// Generous fixed safety bound on raw archive/tree entries read BEFORE the
|
|
48
52
|
// manifest itself is parsed (the exact expected count, base entries plus one
|
|
49
|
-
// per `orchestration` runtime, is known only after
|
|
50
|
-
// contracts register a handful of runtimes; this bound
|
|
51
|
-
// absurd smuggled inventory, it is not the exact-count check.
|
|
52
|
-
const
|
|
53
|
-
const MAX_RAW_BUNDLE_ENTRIES = BUNDLE_ENTRY_COUNT +
|
|
53
|
+
// per `orchestration` and one per `routing` runtime, is known only after
|
|
54
|
+
// manifest decode). Real contracts register a handful of runtimes; this bound
|
|
55
|
+
// only rules out an absurd smuggled inventory, it is not the exact-count check.
|
|
56
|
+
const MAX_RUNTIME_FILE_ENTRIES = 64;
|
|
57
|
+
const MAX_RAW_BUNDLE_ENTRIES = BUNDLE_ENTRY_COUNT + 2 * MAX_RUNTIME_FILE_ENTRIES;
|
|
54
58
|
// setuid/setgid/sticky, any execute bit, or world-writable content is never
|
|
55
59
|
// acceptable for a data-only contract bundle.
|
|
56
60
|
const FORBIDDEN_MODE_BITS = 0o7113;
|
|
@@ -99,7 +103,13 @@ export interface VerifiedProviderContractBundle {
|
|
|
99
103
|
* 1.2.0, where `orchestration` does not exist in the manifest.
|
|
100
104
|
*/
|
|
101
105
|
readonly orchestration: ReadonlyMap<string, string>;
|
|
102
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Verified ODD implementation-routing block, runtime identity -> UTF-8
|
|
108
|
+
* `routing/<runtime>.md` contents. Empty before contract 1.4.0, where
|
|
109
|
+
* `routing` does not exist in the manifest.
|
|
110
|
+
*/
|
|
111
|
+
readonly routing: ReadonlyMap<string, string>;
|
|
112
|
+
/** Canonical path -> exact verified bytes for all entries (base eight plus one per orchestration and routing runtime). */
|
|
103
113
|
readonly entries: ReadonlyMap<string, Buffer>;
|
|
104
114
|
/** Canonical path -> SHA-256 of the exact verified bytes. */
|
|
105
115
|
readonly entrySha256: ReadonlyMap<string, string>;
|
|
@@ -321,23 +331,65 @@ function parseManifestRole(value: unknown, index: number): ManifestRole {
|
|
|
321
331
|
};
|
|
322
332
|
}
|
|
323
333
|
|
|
324
|
-
|
|
334
|
+
// One entry of a runtime-file inventory: `orchestration` (>= 1.2.0) or
|
|
335
|
+
// `routing` (>= 1.4.0). Both share the shape `{ runtime, file }` with the file
|
|
336
|
+
// at `<inventory>/<runtime>.md`.
|
|
337
|
+
interface ManifestRuntimeFileEntry {
|
|
325
338
|
readonly runtime: string;
|
|
326
339
|
readonly file: ManifestFileReference;
|
|
327
340
|
}
|
|
328
341
|
|
|
329
|
-
|
|
330
|
-
|
|
342
|
+
type RuntimeFileInventory = "orchestration" | "routing";
|
|
343
|
+
|
|
344
|
+
function parseRuntimeFileEntry(inventory: RuntimeFileInventory, value: unknown, index: number): ManifestRuntimeFileEntry {
|
|
345
|
+
const where = `manifest.${inventory}[${index}]`;
|
|
331
346
|
const object = requireObject(value, where);
|
|
332
347
|
requireExactKeys(object, ["runtime", "file"], ["runtime", "file"], where);
|
|
333
348
|
const runtime = requireString(object.runtime, `${where}.runtime`);
|
|
334
349
|
if (!RUNTIME_IDENTITY_PATTERN.test(runtime)) invalid(`${where}.runtime ${JSON.stringify(runtime)} is not a runtime identity`);
|
|
335
350
|
const file = parseFileReference(object.file, `${where}.file`);
|
|
336
|
-
const expectedPath =
|
|
351
|
+
const expectedPath = `${inventory}/${runtime}.md`;
|
|
337
352
|
if (file.path !== expectedPath) invalid(`${where}.file.path must be ${JSON.stringify(expectedPath)}`);
|
|
338
353
|
return { runtime, file };
|
|
339
354
|
}
|
|
340
355
|
|
|
356
|
+
// Decodes one runtime-file inventory: absent before the contract minor that
|
|
357
|
+
// introduced it, required from it on, strictly sorted, every runtime
|
|
358
|
+
// registered in manifest.runtimes, and `pi` always present.
|
|
359
|
+
function parseRuntimeFileInventory(
|
|
360
|
+
object: Record<string, unknown>,
|
|
361
|
+
inventory: RuntimeFileInventory,
|
|
362
|
+
requiredSinceMinor: number,
|
|
363
|
+
contractSemver: string,
|
|
364
|
+
minor: number,
|
|
365
|
+
runtimes: readonly string[] | undefined,
|
|
366
|
+
): readonly ManifestRuntimeFileEntry[] {
|
|
367
|
+
const required = minor >= requiredSinceMinor;
|
|
368
|
+
if (!Object.prototype.hasOwnProperty.call(object, inventory)) {
|
|
369
|
+
if (required) invalid(`manifest.${inventory} is required from contract 1.${requiredSinceMinor}.0 (got ${contractSemver} without it)`);
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
if (!required) {
|
|
373
|
+
invalid(`manifest.${inventory} is not part of contract ${contractSemver}; it exists from 1.${requiredSinceMinor}.0`);
|
|
374
|
+
}
|
|
375
|
+
const value = object[inventory];
|
|
376
|
+
if (!Array.isArray(value) || value.length === 0) invalid(`manifest.${inventory} must be a non-empty array`);
|
|
377
|
+
const entries = value.map((entry, index) => parseRuntimeFileEntry(inventory, entry, index));
|
|
378
|
+
for (let position = 0; position < entries.length; position += 1) {
|
|
379
|
+
const entry = entries[position] as ManifestRuntimeFileEntry;
|
|
380
|
+
if (position > 0 && (entries[position - 1] as ManifestRuntimeFileEntry).runtime >= entry.runtime) {
|
|
381
|
+
invalid(`manifest.${inventory} must be strictly sorted by runtime and unique`);
|
|
382
|
+
}
|
|
383
|
+
if (runtimes === undefined || !runtimes.includes(entry.runtime)) {
|
|
384
|
+
invalid(`manifest.${inventory}[${position}].runtime ${JSON.stringify(entry.runtime)} is not registered in manifest.runtimes`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!entries.some((entry) => entry.runtime === PI_RUNTIME_IDENTITY)) {
|
|
388
|
+
invalid(`manifest.${inventory} does not register the ${JSON.stringify(PI_RUNTIME_IDENTITY)} runtime identity`);
|
|
389
|
+
}
|
|
390
|
+
return entries;
|
|
391
|
+
}
|
|
392
|
+
|
|
341
393
|
interface ParsedManifest {
|
|
342
394
|
readonly contractSemver: string;
|
|
343
395
|
readonly major: number;
|
|
@@ -347,14 +399,15 @@ interface ParsedManifest {
|
|
|
347
399
|
readonly runtimes: readonly string[] | undefined;
|
|
348
400
|
readonly readme: ManifestFileReference;
|
|
349
401
|
readonly roles: readonly ManifestRole[];
|
|
350
|
-
readonly orchestration: readonly
|
|
402
|
+
readonly orchestration: readonly ManifestRuntimeFileEntry[];
|
|
403
|
+
readonly routing: readonly ManifestRuntimeFileEntry[];
|
|
351
404
|
}
|
|
352
405
|
|
|
353
406
|
function parseManifest(payload: Buffer): ParsedManifest {
|
|
354
407
|
const object = decodeStrictManifestObject(payload);
|
|
355
408
|
requireExactKeys(
|
|
356
409
|
object,
|
|
357
|
-
["schema", "contract_semver", "transport_capability", "runtimes", "readme", "roles", "orchestration"],
|
|
410
|
+
["schema", "contract_semver", "transport_capability", "runtimes", "readme", "roles", "orchestration", "routing"],
|
|
358
411
|
["schema", "contract_semver", "transport_capability", "readme", "roles"],
|
|
359
412
|
"manifest",
|
|
360
413
|
);
|
|
@@ -398,32 +451,8 @@ function parseManifest(payload: Buffer): ParsedManifest {
|
|
|
398
451
|
invalid(`manifest.runtimes does not register the ${JSON.stringify(PI_RUNTIME_IDENTITY)} runtime identity`);
|
|
399
452
|
}
|
|
400
453
|
|
|
401
|
-
const
|
|
402
|
-
|
|
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
|
-
}
|
|
454
|
+
const orchestration = parseRuntimeFileInventory(object, "orchestration", ORCHESTRATION_REQUIRED_SINCE_MINOR, contractSemver, minor, runtimes);
|
|
455
|
+
const routing = parseRuntimeFileInventory(object, "routing", ROUTING_REQUIRED_SINCE_MINOR, contractSemver, minor, runtimes);
|
|
427
456
|
|
|
428
457
|
if (!Array.isArray(object.roles)) invalid("manifest.roles must be an array");
|
|
429
458
|
const roles = object.roles.map((role, index) => parseManifestRole(role, index));
|
|
@@ -446,6 +475,7 @@ function parseManifest(payload: Buffer): ParsedManifest {
|
|
|
446
475
|
readme: parseFileReference(object.readme, "manifest.readme"),
|
|
447
476
|
roles,
|
|
448
477
|
orchestration,
|
|
478
|
+
routing,
|
|
449
479
|
};
|
|
450
480
|
}
|
|
451
481
|
|
|
@@ -541,8 +571,9 @@ export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<stri
|
|
|
541
571
|
|
|
542
572
|
// The exact expected inventory size is only known once the manifest is
|
|
543
573
|
// decoded: base entries plus exactly one `orchestration/<runtime>.md` per
|
|
544
|
-
// registered orchestration runtime
|
|
545
|
-
|
|
574
|
+
// registered orchestration runtime and one `routing/<runtime>.md` per
|
|
575
|
+
// registered routing runtime.
|
|
576
|
+
const expectedEntryCount = BUNDLE_ENTRY_COUNT + manifest.orchestration.length + manifest.routing.length;
|
|
546
577
|
if (rawEntries.size !== expectedEntryCount) {
|
|
547
578
|
invalid(`bundle inventory has ${rawEntries.size} files; exactly ${expectedEntryCount} are expected`);
|
|
548
579
|
}
|
|
@@ -554,13 +585,18 @@ export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<stri
|
|
|
554
585
|
expected.add(`schemas/${role.id}.schema.json`);
|
|
555
586
|
expected.add(`vectors/${role.id}.json`);
|
|
556
587
|
}
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
588
|
+
const verifyRuntimeFiles = (inventory: RuntimeFileInventory, manifestEntries: readonly ManifestRuntimeFileEntry[]): Map<string, string> => {
|
|
589
|
+
const texts = new Map<string, string>();
|
|
590
|
+
for (const entry of manifestEntries) {
|
|
591
|
+
const path = `${inventory}/${entry.runtime}.md`;
|
|
592
|
+
const payload = verifyFileReference(rawEntries, entry.file, path);
|
|
593
|
+
texts.set(entry.runtime, payload.toString("utf8"));
|
|
594
|
+
expected.add(path);
|
|
595
|
+
}
|
|
596
|
+
return texts;
|
|
597
|
+
};
|
|
598
|
+
const orchestration = verifyRuntimeFiles("orchestration", manifest.orchestration);
|
|
599
|
+
const routing = verifyRuntimeFiles("routing", manifest.routing);
|
|
564
600
|
for (const name of rawEntries.keys()) {
|
|
565
601
|
if (!expected.has(name)) invalid(`bundle has unexpected file ${JSON.stringify(name)}`);
|
|
566
602
|
}
|
|
@@ -591,6 +627,7 @@ export function verifyProviderContractBundleEntries(rawEntries: ReadonlyMap<stri
|
|
|
591
627
|
vectorSha256: role.vector.sha256,
|
|
592
628
|
})),
|
|
593
629
|
orchestration,
|
|
630
|
+
routing,
|
|
594
631
|
entries,
|
|
595
632
|
entrySha256,
|
|
596
633
|
treeSha256: bundleTreeSha256(entrySha256),
|
|
@@ -745,6 +782,18 @@ export const PROVIDER_CAPABILITIES_BASELINE_SCHEMA = "gentle-pi.provider-contrac
|
|
|
745
782
|
export const PROVIDER_ROLES_BASELINE_FILE = "provider-roles.baseline.json";
|
|
746
783
|
export const PROVIDER_CAPABILITIES_BASELINE_FILE = "provider-capabilities.baseline.json";
|
|
747
784
|
|
|
785
|
+
function runtimeFileBaseline(
|
|
786
|
+
bundle: VerifiedProviderContractBundle,
|
|
787
|
+
inventory: RuntimeFileInventory,
|
|
788
|
+
texts: ReadonlyMap<string, string>,
|
|
789
|
+
): { runtime: string; path: string; sha256: string | undefined }[] {
|
|
790
|
+
return [...texts.keys()].sort().map((runtime) => ({
|
|
791
|
+
runtime,
|
|
792
|
+
path: `${inventory}/${runtime}.md`,
|
|
793
|
+
sha256: bundle.entrySha256.get(`${inventory}/${runtime}.md`),
|
|
794
|
+
}));
|
|
795
|
+
}
|
|
796
|
+
|
|
748
797
|
/**
|
|
749
798
|
* Deterministically derives the provider-owned consumer contract surface from
|
|
750
799
|
* a verified bundle. Pi overlays never live in these files; they are pure
|
|
@@ -773,11 +822,8 @@ export function generateProviderContractBaselines(bundle: VerifiedProviderContra
|
|
|
773
822
|
mandatory_capabilities: mandatory,
|
|
774
823
|
runtimes: bundle.runtimes === undefined ? null : [...bundle.runtimes],
|
|
775
824
|
pi_registered: piRuntimeRegistration(bundle).registered,
|
|
776
|
-
orchestration:
|
|
777
|
-
|
|
778
|
-
path: `orchestration/${runtime}.md`,
|
|
779
|
-
sha256: bundle.entrySha256.get(`orchestration/${runtime}.md`),
|
|
780
|
-
})),
|
|
825
|
+
orchestration: runtimeFileBaseline(bundle, "orchestration", bundle.orchestration),
|
|
826
|
+
routing: runtimeFileBaseline(bundle, "routing", bundle.routing),
|
|
781
827
|
};
|
|
782
828
|
return new Map([
|
|
783
829
|
[PROVIDER_ROLES_BASELINE_FILE, `${JSON.stringify(roles, null, 2)}\n`],
|
|
@@ -208,19 +208,19 @@ function enforcePrivateWindowsDacl(path: string, identity: WindowsAclIdentity =
|
|
|
208
208
|
assertPrivateWindowsDacl(path, "directory", true, identity);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
function privateWindowsDacl(path: string, kind: WindowsObjectKind, protectedDacl: boolean, enforce = false): void {
|
|
211
|
+
function privateWindowsDacl(path: string, kind: WindowsObjectKind, protectedDacl: boolean, enforce = false, windowsIdentity?: WindowsAclIdentity): void {
|
|
212
212
|
if (testingWindowsAclAuthority !== undefined) return testingWindowsAclAuthority(path);
|
|
213
|
-
if (enforce) enforcePrivateWindowsDacl(path);
|
|
214
|
-
else assertPrivateWindowsDacl(path, kind, protectedDacl);
|
|
213
|
+
if (enforce) enforcePrivateWindowsDacl(path, windowsIdentity);
|
|
214
|
+
else assertPrivateWindowsDacl(path, kind, protectedDacl, windowsIdentity);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
function privateWindowsCandidateOwnerBoundary(commonDir: string, enforce = false): void {
|
|
217
|
+
function privateWindowsCandidateOwnerBoundary(commonDir: string, enforce = false, windowsIdentity?: WindowsAclIdentity): void {
|
|
218
218
|
const boundary = [commonDir, join(commonDir, "gentle-ai"), join(commonDir, "gentle-ai", "candidate-views")];
|
|
219
219
|
if (testingWindowsAclAuthority !== undefined) {
|
|
220
220
|
for (const path of boundary) testingWindowsAclAuthority(path);
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
|
-
const identity = windowsAclIdentity();
|
|
223
|
+
const identity = windowsIdentity ?? windowsAclIdentity();
|
|
224
224
|
if (enforce) {
|
|
225
225
|
for (const path of boundary) validatePrivateWindowsOwner(windowsOwnerSid(path, "directory"), identity.user);
|
|
226
226
|
for (const path of boundary) enforcePrivateWindowsDacl(path, identity);
|
|
@@ -304,16 +304,23 @@ function directory(path: string, privateMode = false, platform: NodeJS.Platform
|
|
|
304
304
|
return `${stat.dev}:${stat.ino}`;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
function checkedCandidateOwnerParent(commonDir: string, platform: NodeJS.Platform): { parent: string; windowsIdentity?: WindowsAclIdentity } {
|
|
308
308
|
directory(commonDir, false, platform);
|
|
309
309
|
const control = join(commonDir, "gentle-ai");
|
|
310
310
|
directory(control, false, platform);
|
|
311
311
|
const parent = join(commonDir, "gentle-ai", "candidate-views");
|
|
312
312
|
if (platform === "win32") {
|
|
313
313
|
directory(parent, false, platform);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
const windowsIdentity = testingWindowsAclAuthority === undefined ? windowsAclIdentity() : undefined;
|
|
315
|
+
privateWindowsCandidateOwnerBoundary(commonDir, false, windowsIdentity);
|
|
316
|
+
return { parent, windowsIdentity };
|
|
317
|
+
}
|
|
318
|
+
directory(parent, true, platform, true);
|
|
319
|
+
return { parent };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function assertCandidateOwnerParent(commonDir: string, platform: NodeJS.Platform = process.platform): string {
|
|
323
|
+
return checkedCandidateOwnerParent(commonDir, platform).parent;
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
export function prepareCandidateOwnerParent(commonDir: string, platform: NodeJS.Platform = process.platform): string {
|
|
@@ -329,13 +336,13 @@ export function prepareCandidateOwnerParent(commonDir: string, platform: NodeJS.
|
|
|
329
336
|
return assertCandidateOwnerParent(commonDir, platform);
|
|
330
337
|
}
|
|
331
338
|
|
|
332
|
-
function regular(path: string, privateMode = false, platform: NodeJS.Platform = process.platform): string {
|
|
339
|
+
function regular(path: string, privateMode = false, platform: NodeJS.Platform = process.platform, windowsIdentity?: WindowsAclIdentity): string {
|
|
333
340
|
// Keep 64-bit identity exact without a second stat lookup racing the privacy checks.
|
|
334
341
|
const stat = lstatSync(path, { bigint: true });
|
|
335
342
|
const uid = process.getuid?.();
|
|
336
343
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || !samePath(realpathSync(path), path, platform) || stat.size > 16384n ||
|
|
337
344
|
(privateMode && platform !== "win32" && (uid === undefined || stat.uid !== BigInt(uid) || (stat.mode & 0o777n) !== 0o600n))) throw new Error("Unsafe candidate owner file");
|
|
338
|
-
if (privateMode && platform === "win32") privateWindowsDacl(path, "file", false);
|
|
345
|
+
if (privateMode && platform === "win32") privateWindowsDacl(path, "file", false, false, windowsIdentity);
|
|
339
346
|
return `${stat.dev}:${stat.ino}`;
|
|
340
347
|
}
|
|
341
348
|
|
|
@@ -347,27 +354,27 @@ function syncDirectory(path: string): void {
|
|
|
347
354
|
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
348
355
|
}
|
|
349
356
|
|
|
350
|
-
function removeExactPrivateFile(path: string, identity: string, platform: NodeJS.Platform): void {
|
|
357
|
+
function removeExactPrivateFile(path: string, identity: string, platform: NodeJS.Platform, windowsIdentity?: WindowsAclIdentity): void {
|
|
351
358
|
try {
|
|
352
|
-
if (regular(path, true, platform) !== identity) return;
|
|
359
|
+
if (regular(path, true, platform, windowsIdentity) !== identity) return;
|
|
353
360
|
unlinkSync(path);
|
|
354
361
|
syncDirectory(dirname(path));
|
|
355
362
|
} catch { /* An unproven or replaced file survives. */ }
|
|
356
363
|
}
|
|
357
364
|
|
|
358
|
-
function exclusiveFile(path: string, content: string, platform: NodeJS.Platform = process.platform, rollbackOnFailure = false): string {
|
|
365
|
+
function exclusiveFile(path: string, content: string, platform: NodeJS.Platform = process.platform, rollbackOnFailure = false, windowsIdentity?: WindowsAclIdentity): string {
|
|
359
366
|
let identity: string | undefined;
|
|
360
367
|
try {
|
|
361
368
|
const fd = openSync(path, "wx", 0o600);
|
|
362
369
|
try {
|
|
363
|
-
identity = regular(path, true, platform);
|
|
370
|
+
identity = regular(path, true, platform, windowsIdentity);
|
|
364
371
|
writeFileSync(fd, content);
|
|
365
372
|
fsyncSync(fd);
|
|
366
373
|
} finally { closeSync(fd); }
|
|
367
374
|
syncDirectory(dirname(path));
|
|
368
375
|
return identity;
|
|
369
376
|
} catch (error) {
|
|
370
|
-
if (rollbackOnFailure && identity !== undefined) removeExactPrivateFile(path, identity, platform);
|
|
377
|
+
if (rollbackOnFailure && identity !== undefined) removeExactPrivateFile(path, identity, platform, windowsIdentity);
|
|
371
378
|
throw error;
|
|
372
379
|
}
|
|
373
380
|
}
|
|
@@ -375,7 +382,10 @@ function exclusiveFile(path: string, content: string, platform: NodeJS.Platform
|
|
|
375
382
|
function markerPath(root: string): string { return `${root}.owner.json`; }
|
|
376
383
|
|
|
377
384
|
export function createCandidateOwner(commonDir: string, root: string, platform: NodeJS.Platform = process.platform): CandidateViewOwner {
|
|
378
|
-
|
|
385
|
+
// Reuse only identity within this module-controlled synchronous creation.
|
|
386
|
+
// Owner/DACL observations stay fresh, including rollback. Nothing is retained
|
|
387
|
+
// across calls or passed into callback-based removal/sweeping operations.
|
|
388
|
+
const { parent, windowsIdentity } = checkedCandidateOwnerParent(commonDir, platform);
|
|
379
389
|
const uuid = basename(root);
|
|
380
390
|
if (!UUID.test(uuid) || root !== join(parent, uuid) || lstatSync(root, { throwIfNoEntry: false })) throw new Error("Unsafe candidate owner root");
|
|
381
391
|
const owner: CandidateViewOwner = { version: 1, uuid, token: randomUUID(), pid: process.pid, host: localHost(), root, commonDir };
|
|
@@ -383,11 +393,11 @@ export function createCandidateOwner(commonDir: string, root: string, platform:
|
|
|
383
393
|
const marker = markerPath(root);
|
|
384
394
|
let markerIdentity: string | undefined;
|
|
385
395
|
try {
|
|
386
|
-
markerIdentity = exclusiveFile(marker, JSON.stringify(owner), platform, true);
|
|
396
|
+
markerIdentity = exclusiveFile(marker, JSON.stringify(owner), platform, true, windowsIdentity);
|
|
387
397
|
syncDirectory(dirname(parent));
|
|
388
398
|
syncDirectory(commonDir);
|
|
389
399
|
} catch (error) {
|
|
390
|
-
if (markerIdentity !== undefined) removeExactPrivateFile(marker, markerIdentity, platform);
|
|
400
|
+
if (markerIdentity !== undefined) removeExactPrivateFile(marker, markerIdentity, platform, windowsIdentity);
|
|
391
401
|
throw error;
|
|
392
402
|
}
|
|
393
403
|
return Object.freeze(owner);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, utimesSync } from "node:fs";
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, utimesSync, type Stats } from "node:fs";
|
|
4
4
|
import { mkdtempSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -650,6 +650,32 @@ function makeReadonly(root: string, entries: readonly CandidateViewEntry[]): voi
|
|
|
650
650
|
for (const directory of candidateDirectories(root, entries)) chmodSync(directory, 0o555);
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
// macOS File Provider (iCloud-synced Desktop/Documents) asynchronously resets
|
|
654
|
+
// directory modes to exactly 0700 while leaving file modes intact, so a view
|
|
655
|
+
// made read-only at creation later reads as writable. Only that exact shape on
|
|
656
|
+
// a directory this user owns is re-tightened; any group or other bit still
|
|
657
|
+
// refuses. Owner-only write grants nothing a same-user process could not do by
|
|
658
|
+
// chmod anyway, and the caller then proves the directory holds exactly the
|
|
659
|
+
// frozen children, closing the ignored-file injection the untracked scan misses.
|
|
660
|
+
function repairFileProviderDirectoryMode(directory: string, metadata: Stats, platform: NodeJS.Platform): boolean {
|
|
661
|
+
if (platform === "win32" || !metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o7777) !== 0o700) return false;
|
|
662
|
+
if (typeof process.getuid !== "function" || metadata.uid !== process.getuid()) return false;
|
|
663
|
+
chmodSync(directory, 0o555);
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function expectedDirectoryChildren(root: string, entries: readonly CandidateViewEntry[]): ReadonlyMap<string, ReadonlySet<string>> {
|
|
668
|
+
const children = new Map<string, Set<string>>([[root, new Set([".git"])]]);
|
|
669
|
+
for (const entry of entries) {
|
|
670
|
+
for (let path = join(root, entry.path); path !== root && isWithin(root, path); path = dirname(path)) {
|
|
671
|
+
const parent = dirname(path);
|
|
672
|
+
if (!children.has(parent)) children.set(parent, new Set());
|
|
673
|
+
children.get(parent)!.add(basename(path));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return children;
|
|
677
|
+
}
|
|
678
|
+
|
|
653
679
|
function makeWritableForCleanup(path: string): void {
|
|
654
680
|
const entry = lstatSync(path, { throwIfNoEntry: false });
|
|
655
681
|
if (!entry || entry.isSymbolicLink()) return;
|
|
@@ -1050,8 +1076,16 @@ function assertRecordSafe(record: CandidateViewRecord, platform: NodeJS.Platform
|
|
|
1050
1076
|
if (!isWithin(record.parent, root) || !existsSync(root)) throw new CandidateViewError("candidate view is missing or moved");
|
|
1051
1077
|
const rootStat = lstatSync(root);
|
|
1052
1078
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || realpathSync(root) !== root) throw new CandidateViewError("candidate view root is unsafe");
|
|
1079
|
+
let expectedChildren: ReadonlyMap<string, ReadonlySet<string>> | undefined;
|
|
1053
1080
|
for (const directory of candidateDirectories(root, record.entries)) {
|
|
1054
|
-
|
|
1081
|
+
let metadata = lstatSync(directory);
|
|
1082
|
+
if (repairFileProviderDirectoryMode(directory, metadata, platform)) {
|
|
1083
|
+
expectedChildren ??= expectedDirectoryChildren(root, record.entries);
|
|
1084
|
+
const expected = expectedChildren.get(directory) ?? new Set<string>();
|
|
1085
|
+
const actual = readdirSync(directory);
|
|
1086
|
+
if (actual.length !== expected.size || actual.some((name) => !expected.has(name))) throw new CandidateViewError("candidate view directory contains unexpected entries");
|
|
1087
|
+
metadata = lstatSync(directory);
|
|
1088
|
+
}
|
|
1055
1089
|
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o222) !== 0) throw new CandidateViewError("candidate view directory is unsafe or writable");
|
|
1056
1090
|
}
|
|
1057
1091
|
const gitFile = lstatSync(join(root, ".git"));
|
|
@@ -267,8 +267,9 @@ export type VerificationTier = (typeof VERIFICATION_TIER)[keyof typeof VERIFICAT
|
|
|
267
267
|
// ---------------------------------------------------------------------------
|
|
268
268
|
// Agent-raised risk (gentle-shell#1494). The agent that made the change may
|
|
269
269
|
// raise its risk to high by citing one item of the high-risk list in the
|
|
270
|
-
// always-on Task Size section; it can never lower a tier.
|
|
271
|
-
// the
|
|
270
|
+
// always-on Task Size section; it can never lower a tier. That escalation is
|
|
271
|
+
// the only way a plan gets an independent verifier: a native high or a failed
|
|
272
|
+
// assessment informs but never adds one by itself.
|
|
272
273
|
// ---------------------------------------------------------------------------
|
|
273
274
|
|
|
274
275
|
export const HIGH_RISK_ITEMS = Object.freeze({
|
|
@@ -305,10 +306,17 @@ export function decodeAgentRiskEscalation(value: unknown): AgentRiskEscalation {
|
|
|
305
306
|
return Object.freeze({ item: item as HighRiskItem, reason });
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
/**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
/**
|
|
310
|
+
* The risk tier the verification plan follows. The independent verifier is
|
|
311
|
+
* for concrete occasions only: the plan is high only when the agent escalated
|
|
312
|
+
* a named Task Size high-risk item. Without an escalation a native passive
|
|
313
|
+
* stays passive, and every other native tier -- medium, high, or unassessable
|
|
314
|
+
* (a failed or unavailable assessment) -- is medium: it informs, but adds no
|
|
315
|
+
* verifier by itself. The native tier stays visible to callers as `nativeRisk`.
|
|
316
|
+
*/
|
|
317
|
+
export function verificationRisk(nativeRisk: VerificationTier, escalation: AgentRiskEscalation | undefined): VerificationTier {
|
|
318
|
+
if (escalation !== undefined) return VERIFICATION_TIER.HIGH;
|
|
319
|
+
return nativeRisk === VERIFICATION_TIER.PASSIVE ? VERIFICATION_TIER.PASSIVE : VERIFICATION_TIER.MEDIUM;
|
|
312
320
|
}
|
|
313
321
|
|
|
314
322
|
export const RDD_LINE = {
|
|
@@ -342,7 +350,9 @@ export type NativeReviewOutcome = (typeof NATIVE_REVIEW_OUTCOME)[keyof typeof NA
|
|
|
342
350
|
|
|
343
351
|
export interface VerificationPlanInput {
|
|
344
352
|
readonly rddLine: RddLine;
|
|
353
|
+
/** The plan risk from `verificationRisk`, not the raw native tier. */
|
|
345
354
|
readonly risk: VerificationTier;
|
|
355
|
+
/** Reported context only: the writer's model or effort never changes the plan. */
|
|
346
356
|
readonly writerProfile: WriterProfile;
|
|
347
357
|
/**
|
|
348
358
|
* Native review outcome for the current candidate. Only consulted when
|
|
@@ -366,7 +376,7 @@ export interface VerificationPlan {
|
|
|
366
376
|
* naming which branch fired, up to and including the word "and"; the
|
|
367
377
|
* tier-specific clause is appended after it.
|
|
368
378
|
*/
|
|
369
|
-
function riskGatedPlan(gatePrefix: string, risk: VerificationTier
|
|
379
|
+
function riskGatedPlan(gatePrefix: string, risk: VerificationTier): VerificationPlan {
|
|
370
380
|
if (risk === VERIFICATION_TIER.PASSIVE) {
|
|
371
381
|
return Object.freeze({
|
|
372
382
|
writerSelfVerification: false,
|
|
@@ -376,26 +386,22 @@ function riskGatedPlan(gatePrefix: string, risk: VerificationTier, writerProfile
|
|
|
376
386
|
});
|
|
377
387
|
}
|
|
378
388
|
|
|
379
|
-
if (risk === VERIFICATION_TIER.
|
|
380
|
-
const smallModelBias = writerProfile === WRITER_PROFILE.SMALL;
|
|
389
|
+
if (risk === VERIFICATION_TIER.HIGH) {
|
|
381
390
|
return Object.freeze({
|
|
382
391
|
writerSelfVerification: true,
|
|
383
392
|
structuralReadbackOnly: false,
|
|
384
|
-
independentVerifier:
|
|
385
|
-
reason:
|
|
386
|
-
? `${gatePrefix} the native assessment reports medium risk, but the small-model bias raises it one tier to high for verification purposes: the writer self-verifies and a separate independent verifier also runs.`
|
|
387
|
-
: `${gatePrefix} the native assessment reports medium risk with a large writer profile: the writer's self-verification stands and no separate verifier is required.`,
|
|
393
|
+
independentVerifier: true,
|
|
394
|
+
reason: `${gatePrefix} an agent escalation names a Task Size high-risk item: the writer self-verifies and a separate independent verifier runs.`,
|
|
388
395
|
});
|
|
389
396
|
}
|
|
390
397
|
|
|
391
|
-
//
|
|
398
|
+
// medium, or an unassessable tier that did not go through verificationRisk:
|
|
399
|
+
// both inform only.
|
|
392
400
|
return Object.freeze({
|
|
393
401
|
writerSelfVerification: true,
|
|
394
402
|
structuralReadbackOnly: false,
|
|
395
|
-
independentVerifier:
|
|
396
|
-
reason: risk
|
|
397
|
-
? `${gatePrefix} the native risk assessment could not be produced, so the candidate is treated as high risk: the writer self-verifies and a separate independent verifier always runs.`
|
|
398
|
-
: `${gatePrefix} the native assessment reports high risk: the writer self-verifies and a separate independent verifier always runs.`,
|
|
403
|
+
independentVerifier: false,
|
|
404
|
+
reason: `${gatePrefix} no Task Size high-risk item was escalated: the writer's self-verification stands and no separate verifier runs, whatever the native tier or the model that wrote the change.`,
|
|
399
405
|
});
|
|
400
406
|
}
|
|
401
407
|
|
|
@@ -416,24 +422,23 @@ function nonClosedOutcomeClause(outcome: NativeReviewOutcome): string {
|
|
|
416
422
|
|
|
417
423
|
/**
|
|
418
424
|
* Computes who verifies a delegated writer's change. Verification follows the
|
|
419
|
-
*
|
|
425
|
+
* plan risk tier whatever the RDD line and native review outcome
|
|
420
426
|
* (verify-always-rdd-high S1, superseding the gentle-pi#662/#668 on-path): the
|
|
421
427
|
* native review is an additional outside view and never replaces or skips the
|
|
422
428
|
* tier's verification. `nativeReviewOutcome` only shapes the reason text.
|
|
423
429
|
*
|
|
430
|
+
* `risk` is the plan risk from `verificationRisk`; `writerProfile` is never
|
|
431
|
+
* consulted, so the model or effort that wrote the change never adds a
|
|
432
|
+
* verifier.
|
|
433
|
+
*
|
|
424
434
|
* - `passive`: structural readback by the parent only; no verifier, no tests.
|
|
425
|
-
* - `medium
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* function (see `resolveWriterProfile`), so an unrecognized profile never
|
|
430
|
-
* lowers a tier below what a known small model would get.
|
|
431
|
-
* - `high` or `unassessable` (a failed or unrecognized native assessment is
|
|
432
|
-
* always treated as high): the writer self-verifies and an independent
|
|
433
|
-
* verifier always runs.
|
|
435
|
+
* - `medium` (also an `unassessable` tier passed directly): the writer
|
|
436
|
+
* self-verifies; no separate verifier.
|
|
437
|
+
* - `high` (only through an agent escalation naming a Task Size high-risk
|
|
438
|
+
* item): the writer self-verifies and an independent verifier runs.
|
|
434
439
|
*/
|
|
435
440
|
export function verificationPlan(input: VerificationPlanInput): VerificationPlan {
|
|
436
|
-
const { rddLine, risk
|
|
441
|
+
const { rddLine, risk } = input;
|
|
437
442
|
|
|
438
443
|
if (rddLine === RDD_LINE.ON) {
|
|
439
444
|
const outcome = input.nativeReviewOutcome ?? NATIVE_REVIEW_OUTCOME.UNKNOWN;
|
|
@@ -443,14 +448,13 @@ export function verificationPlan(input: VerificationPlanInput): VerificationPlan
|
|
|
443
448
|
return riskGatedPlan(
|
|
444
449
|
`receipt-driven development is on and ${clause}; the native review is an additional outside view that never replaces verification, so the risk-gated path applies exactly as receipt-driven development off, and`,
|
|
445
450
|
risk,
|
|
446
|
-
writerProfile,
|
|
447
451
|
);
|
|
448
452
|
}
|
|
449
453
|
|
|
450
454
|
// rddLine is "off" or "unknown" here -- both gate on the native risk
|
|
451
455
|
// assessment identically, so an unknown RDD line never lowers a tier, and
|
|
452
456
|
// neither line ever consults nativeReviewOutcome.
|
|
453
|
-
return riskGatedPlan(`receipt-driven development is ${rddLine} and`, risk
|
|
457
|
+
return riskGatedPlan(`receipt-driven development is ${rddLine} and`, risk);
|
|
454
458
|
}
|
|
455
459
|
|
|
456
460
|
// ---------------------------------------------------------------------------
|
|
@@ -475,15 +479,16 @@ export interface WriterModelProfileLike {
|
|
|
475
479
|
const SMALL_MODEL_ID_TOKEN = /(?:^|[-_./:\s])mini(?:$|[-_./:\s])/i;
|
|
476
480
|
|
|
477
481
|
/**
|
|
478
|
-
* A writer profile counts as a "small model"
|
|
482
|
+
* A writer profile counts as a "small model" (reported by ASSESS as
|
|
483
|
+
* `writerProfile`; it never changes the verification plan) when:
|
|
479
484
|
* - its resolved effort is `low`, or
|
|
480
485
|
* - its resolved model id carries `mini` as a whole token (see
|
|
481
486
|
* `SMALL_MODEL_ID_TOKEN`), or
|
|
482
487
|
* - it carries NO signal at all -- no profile object, or a profile with
|
|
483
488
|
* neither a model id nor a resolved effort. An unknown or omitted writer
|
|
484
|
-
* profile fails closed to small rather than defaulting to large
|
|
485
|
-
*
|
|
486
|
-
*
|
|
489
|
+
* profile fails closed to small rather than defaulting to large. The
|
|
490
|
+
* result is reported in the assessment only: the verification plan never
|
|
491
|
+
* reads the writer profile, so it cannot add or remove a verifier.
|
|
487
492
|
*
|
|
488
493
|
* A profile that carries a model id or an effort, just not a matching one
|
|
489
494
|
* (for example `{model: {id: "claude-sonnet-5"}, thinking: "high"}`), is
|
|
@@ -501,9 +506,9 @@ export function isSmallWriterProfile(profile: WriterModelProfileLike | undefined
|
|
|
501
506
|
}
|
|
502
507
|
|
|
503
508
|
/**
|
|
504
|
-
* Maps a resolved writer profile to the `writerProfile
|
|
505
|
-
*
|
|
506
|
-
* `
|
|
509
|
+
* Maps a resolved writer profile to the reported `writerProfile`. An unknown
|
|
510
|
+
* or omitted profile resolves to `"small"`, exactly like
|
|
511
|
+
* `isSmallWriterProfile` (gentle-pi#662).
|
|
507
512
|
*/
|
|
508
513
|
export function resolveWriterProfile(profile: WriterModelProfileLike | undefined): WriterProfile {
|
|
509
514
|
return isSmallWriterProfile(profile) ? WRITER_PROFILE.SMALL : WRITER_PROFILE.LARGE;
|
|
@@ -15,6 +15,7 @@ export interface GitCommandOptions {
|
|
|
15
15
|
timeout: number;
|
|
16
16
|
maxBuffer: number;
|
|
17
17
|
windowsHide?: boolean;
|
|
18
|
+
stdio?: ["ignore", "pipe", "pipe"];
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export type GitAsyncRunner = (command: string, args: readonly string[], options: GitCommandOptions) => Promise<{ stdout: string }>;
|
|
@@ -133,7 +134,8 @@ export function resolveCanonicalGitRepositoryIdentitySync(
|
|
|
133
134
|
run: GitSyncRunner = execFileSync as unknown as GitSyncRunner,
|
|
134
135
|
): string | undefined {
|
|
135
136
|
try {
|
|
136
|
-
|
|
137
|
+
// execFileSync otherwise echoes stderr on failure, even when caught below.
|
|
138
|
+
const output = run("git", ["-C", cwd, "rev-parse", "--git-common-dir"], { encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
137
139
|
if (output.length === 0 || output.includes("\n") || output.includes("\r")) return undefined;
|
|
138
140
|
return canonicalRepositoryIdentity(realpathSync(resolve(cwd, output)));
|
|
139
141
|
} catch {
|