gentle-pi 2.5.0 → 2.6.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 +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
+
import childProcess from "node:child_process";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
import { syncBuiltinESMExports } from "node:module";
|
|
6
7
|
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { tmpdir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
9
10
|
import test from "node:test";
|
|
10
11
|
import { gzipSync } from "node:zlib";
|
|
11
12
|
import {
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
readCandidateContextManifestPage,
|
|
22
23
|
type NativeCandidateProjectionDescriptor,
|
|
23
24
|
} from "../lib/review-candidate-view.ts";
|
|
25
|
+
import { assertCandidateOwnerParent, assertTrustedWindowsOwner, prepareCandidateOwnerParent, setWindowsAclAuthorityForTesting, validatePrivateWindowsDacl, validatePrivateWindowsOwner, WindowsDaclValidationError, WindowsOwnerValidationError } from "../lib/review-candidate-view-owner.ts";
|
|
24
26
|
|
|
25
27
|
function git(cwd: string, ...arguments_: string[]): string {
|
|
26
28
|
return execFileSync("git", arguments_, { cwd, encoding: "utf8" }).trim();
|
|
@@ -29,7 +31,7 @@ function git(cwd: string, ...arguments_: string[]): string {
|
|
|
29
31
|
function repository(t: test.TestContext): string {
|
|
30
32
|
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-candidate-view-"));
|
|
31
33
|
t.after(() => {
|
|
32
|
-
execFileSync("chmod", ["-R", "u+rwx", cwd]);
|
|
34
|
+
if (process.platform !== "win32") execFileSync("chmod", ["-R", "u+rwx", cwd]);
|
|
33
35
|
rmSync(cwd, { recursive: true, force: true });
|
|
34
36
|
});
|
|
35
37
|
git(cwd, "init", "-b", "main");
|
|
@@ -41,6 +43,11 @@ function repository(t: test.TestContext): string {
|
|
|
41
43
|
|
|
42
44
|
function ownerMarker(root: string): string { return `${root}.owner.json`; }
|
|
43
45
|
|
|
46
|
+
function mockWindowsAcl(t: test.TestContext): void {
|
|
47
|
+
setWindowsAclAuthorityForTesting(() => {});
|
|
48
|
+
t.after(() => setWindowsAclAuthorityForTesting());
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
function orphanFixture(t: test.TestContext) {
|
|
45
52
|
const cwd = repository(t);
|
|
46
53
|
const registry = new CandidateViewRegistry();
|
|
@@ -75,9 +82,9 @@ test("candidate ownership is private and durable before worktree add; ordinary c
|
|
|
75
82
|
assert.equal(owner.root, root);
|
|
76
83
|
assert.equal(owner.pid, process.pid);
|
|
77
84
|
assert.match(owner.token, /^[0-9a-f-]{36}$/);
|
|
78
|
-
assert.equal(lstatSync(marker).mode & 0o777, 0o600);
|
|
85
|
+
if (process.platform !== "win32") assert.equal(lstatSync(marker).mode & 0o777, 0o600);
|
|
79
86
|
assert.equal(existsSync(root), false);
|
|
80
|
-
assert.ok(syncs >= 4, "marker and every newly created ancestor must be durable before add");
|
|
87
|
+
assert.ok(syncs >= (process.platform === "win32" ? 1 : 4), "marker and every newly created ancestor must be durable before add");
|
|
81
88
|
observed = true;
|
|
82
89
|
}
|
|
83
90
|
return execFileSync(file, args, options);
|
|
@@ -89,6 +96,294 @@ test("candidate ownership is private and durable before worktree add; ordinary c
|
|
|
89
96
|
assert.equal(existsSync(ownerMarker(view.root)), false);
|
|
90
97
|
});
|
|
91
98
|
|
|
99
|
+
test("private candidate owner accepts safe Windows artifacts", (t) => {
|
|
100
|
+
mockWindowsAcl(t);
|
|
101
|
+
const view = new CandidateViewRegistry(undefined, "win32").create({ contributorRoot: repository(t) });
|
|
102
|
+
try {
|
|
103
|
+
assert.equal(lstatSync(ownerMarker(view.root)).isFile(), true);
|
|
104
|
+
assert.doesNotThrow(() => view.verify());
|
|
105
|
+
} finally {
|
|
106
|
+
view.cleanup();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("private candidate owner accepts a canonical Windows worktree registration spelling", { skip: process.platform !== "win32" }, (t) => {
|
|
111
|
+
let root: string | undefined;
|
|
112
|
+
const registry = new CandidateViewRegistry((file, arguments_, options) => {
|
|
113
|
+
const output = execFileSync(file, arguments_, options);
|
|
114
|
+
if (root !== undefined && arguments_[0] === "worktree" && arguments_[1] === "list" && typeof output === "string") {
|
|
115
|
+
return output.replace(`worktree ${root}\0`, `worktree \\\\?\\${root}\0`);
|
|
116
|
+
}
|
|
117
|
+
return output;
|
|
118
|
+
}, "win32");
|
|
119
|
+
const view = registry.create({ contributorRoot: repository(t) });
|
|
120
|
+
root = view.root;
|
|
121
|
+
view.cleanup();
|
|
122
|
+
assert.equal(existsSync(root), false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("private candidate owner rejects an untrusted conditional Windows allow ACE", () => {
|
|
126
|
+
assert.throws(
|
|
127
|
+
() => validatePrivateWindowsDacl("D:P(A;OICI;FA;;;S-1-5-21-1)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(XA;OICI;FA;;;S-1-5-21-2)", "S-1-5-21-1", true),
|
|
128
|
+
(error: unknown) => error instanceof WindowsDaclValidationError && error.reason === "ace-type" && !error.message.includes("S-1-5-21-2"),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("private candidate owner reports bounded Windows DACL mismatch reasons", () => {
|
|
133
|
+
const dacl = "D:P(A;OICI;FA;;;S-1-5-21-1)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)";
|
|
134
|
+
for (const [reason, invalid] of [["ace-flags", dacl.replace("OICI", "OI")], ["ace-rights", dacl.replace("FA", "FR")], ["ace-reserved-fields", dacl.replace(";;;S-1-5-21-1", ";object;;S-1-5-21-1")], ["ace-trustee", dacl.replace("S-1-5-21-1", "S-1-5-21-2")], ["ace-duplicate", `${dacl}(A;OICI;FA;;;SY)`], ["ace-count", dacl.replace("(A;OICI;FA;;;BA)", "")]] as const) {
|
|
135
|
+
assert.throws(() => validatePrivateWindowsDacl(invalid, "S-1-5-21-1", true), (error: unknown) => error instanceof WindowsDaclValidationError && error.reason === reason && error.message === `Windows DACL validation failed: ${reason}${reason === "ace-trustee" ? " (sid)" : ""}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("private candidate owner reports bounded Windows DACL mismatch reasons for trustees", () => {
|
|
140
|
+
const dacl = "D:P(A;OICI;FA;;;S-1-5-21-1)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)";
|
|
141
|
+
for (const [trustee, classification] of [["OW", "OW"], ["CO", "CO"], ["BU", "BU"], ["AU", "AU"], ["WD", "WD"], ["LA", "LA"], ["S-1-5-21-999-888-777-666", "sid"], ["unknown", "other"], ["", "missing"]] as const) {
|
|
142
|
+
assert.throws(() => validatePrivateWindowsDacl(dacl.replace("S-1-5-21-1", trustee), "S-1-5-21-1", true), (error: unknown) => error instanceof WindowsDaclValidationError && error.reason === "ace-trustee" && error.trustee === classification && error.message === `Windows DACL validation failed: ace-trustee (${classification})` && (trustee === classification || trustee === "" || !error.message.includes(trustee)));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("private candidate owner accepts LA for the exact local Administrator SID", () => {
|
|
147
|
+
assert.doesNotThrow(() => validatePrivateWindowsDacl("D:P(A;OICI;FA;;;LA)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)", "S-1-5-21-1-2-3-500", true, "S-1-5-21-1-2-3-500"));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("private candidate owner rejects LA for a non-local RID-500 current user when the local Administrator differs", () => {
|
|
151
|
+
assert.throws(() => validatePrivateWindowsDacl("D:P(A;OICI;FA;;;LA)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)", "S-1-5-21-1-2-3-500", true, "S-1-5-21-4-5-6-500"), (error: unknown) => error instanceof WindowsDaclValidationError && error.reason === "ace-trustee" && error.trustee === "LA");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("private candidate owner rejects raw and LA local Administrator grants as duplicates", () => {
|
|
155
|
+
assert.throws(() => validatePrivateWindowsDacl("D:P(A;OICI;FA;;;S-1-5-21-1-2-3-500)(A;OICI;FA;;;LA)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)", "S-1-5-21-1-2-3-500", true, "S-1-5-21-1-2-3-500"), (error: unknown) => error instanceof WindowsDaclValidationError && error.reason === "ace-duplicate");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("private candidate owner fails closed when local Administrator resolution is unavailable", () => {
|
|
159
|
+
assert.throws(() => validatePrivateWindowsDacl("D:P(A;OICI;FA;;;LA)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)", "S-1-5-21-1-2-3-500", true), (error: unknown) => error instanceof WindowsDaclValidationError && error.reason === "ace-trustee" && error.trustee === "LA");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("private candidate owner accepts only trusted Windows owners", () => {
|
|
163
|
+
for (const owner of ["S-1-5-21-1-2-3-1001", "S-1-5-18", "S-1-5-32-544"]) assert.doesNotThrow(() => validatePrivateWindowsOwner(owner, "S-1-5-21-1-2-3-1001"));
|
|
164
|
+
assert.throws(() => validatePrivateWindowsOwner("S-1-5-21-4-5-6-1002", "S-1-5-21-1-2-3-1001"), (error: unknown) => error instanceof WindowsOwnerValidationError && !error.message.includes("S-1-5-21-4-5-6-1002"));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("private candidate owner fails closed when Windows owner resolution is unavailable", () => {
|
|
168
|
+
assert.throws(() => validatePrivateWindowsOwner(undefined, "S-1-5-21-1-2-3-1001"), WindowsOwnerValidationError);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("private candidate owner rejects a different Windows owner at the ancestor replacement boundary", (t) => {
|
|
172
|
+
const cwd = repository(t), commonDir = join(cwd, ".git"), control = join(commonDir, "gentle-ai"), parent = join(control, "candidate-views");
|
|
173
|
+
mkdirSync(parent, { recursive: true });
|
|
174
|
+
setWindowsAclAuthorityForTesting((path) => {
|
|
175
|
+
if (path === control) validatePrivateWindowsOwner("S-1-5-21-4-5-6-1002", "S-1-5-21-1-2-3-1001");
|
|
176
|
+
});
|
|
177
|
+
t.after(() => setWindowsAclAuthorityForTesting());
|
|
178
|
+
assert.throws(() => prepareCandidateOwnerParent(commonDir, "win32"), (error: unknown) => error instanceof WindowsOwnerValidationError && error.owner === "sid");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("private candidate owner revalidates every Windows ancestor replacement boundary", (t) => {
|
|
182
|
+
const cwd = repository(t), commonDir = join(cwd, ".git"), control = join(commonDir, "gentle-ai"), parent = join(control, "candidate-views");
|
|
183
|
+
mkdirSync(parent, { recursive: true });
|
|
184
|
+
const checked: string[] = [];
|
|
185
|
+
setWindowsAclAuthorityForTesting((path) => checked.push(path));
|
|
186
|
+
t.after(() => setWindowsAclAuthorityForTesting());
|
|
187
|
+
assert.equal(assertCandidateOwnerParent(commonDir, "win32"), parent);
|
|
188
|
+
assert.deepEqual(checked, [commonDir, control, parent]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("private candidate owner rejects a controlled foreign Windows owner", { skip: process.platform !== "win32" }, (t) => {
|
|
192
|
+
const path = mkdtempSync(join(tmpdir(), "gentle-pi-foreign-owner-"));
|
|
193
|
+
t.after(() => rmSync(path, { recursive: true, force: true }));
|
|
194
|
+
try {
|
|
195
|
+
execFileSync(realpathSync.native("\\\\?\\GLOBALROOT\\SystemRoot\\System32\\icacls.exe"), [path, "/setowner", "*S-1-5-32-545"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
196
|
+
} catch {
|
|
197
|
+
if (process.env.GITHUB_ACTIONS === "true" || process.env.CI === "true") assert.fail("Windows CI could not assign BUILTIN\\Users as the controlled temporary directory owner");
|
|
198
|
+
t.skip("Current Windows token cannot assign BUILTIN\\Users as the controlled temporary directory owner");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
assert.throws(() => assertTrustedWindowsOwner(path, "directory"), (error: unknown) => error instanceof WindowsOwnerValidationError && error.owner === "sid");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("private candidate owner reads trusted Windows directory and marker file owners", { skip: process.platform !== "win32" }, (t) => {
|
|
205
|
+
const cwd = repository(t), commonDir = join(cwd, ".git"), parent = join(commonDir, "gentle-ai", "candidate-views");
|
|
206
|
+
const view = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
207
|
+
try {
|
|
208
|
+
assert.doesNotThrow(() => assertTrustedWindowsOwner(parent, "directory"));
|
|
209
|
+
assert.doesNotThrow(() => assertTrustedWindowsOwner(ownerMarker(view.root), "file"));
|
|
210
|
+
assert.doesNotThrow(() => view.verify());
|
|
211
|
+
} finally {
|
|
212
|
+
view.cleanup();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("private candidate owner removes unrelated explicit Windows grants during enforcement", { skip: process.platform !== "win32" }, (t) => {
|
|
217
|
+
const cwd = repository(t), commonDir = join(cwd, ".git"), parent = join(commonDir, "gentle-ai", "candidate-views");
|
|
218
|
+
const initial = new CandidateViewRegistry().create({ contributorRoot: cwd });
|
|
219
|
+
initial.cleanup();
|
|
220
|
+
const system = process.env.SystemRoot!;
|
|
221
|
+
execFileSync(`${system}\\System32\\icacls.exe`, [parent, "/grant", "*S-1-5-32-545:(OI)(CI)F"], { encoding: "utf8" });
|
|
222
|
+
const archive = ".candidate-owner-acl.txt";
|
|
223
|
+
try {
|
|
224
|
+
execFileSync(`${system}\\System32\\icacls.exe`, [parent, "/save", archive, "/c"], { cwd: parent, encoding: "utf8" });
|
|
225
|
+
const before = readFileSync(join(parent, archive), "utf16le").match(/D:[^\r\n]+/)?.[0];
|
|
226
|
+
assert.ok(before);
|
|
227
|
+
assert.match(before, /;;;(?:BU|S-1-5-32-545)\)/i);
|
|
228
|
+
assert.doesNotThrow(() => prepareCandidateOwnerParent(commonDir));
|
|
229
|
+
execFileSync(`${system}\\System32\\icacls.exe`, [parent, "/save", archive, "/c"], { cwd: parent, encoding: "utf8" });
|
|
230
|
+
const after = readFileSync(join(parent, archive), "utf16le").match(/D:[^\r\n]+/)?.[0];
|
|
231
|
+
assert.ok(after);
|
|
232
|
+
assert.doesNotMatch(after, /;;;(?:BU|S-1-5-32-545)\)/i);
|
|
233
|
+
} finally {
|
|
234
|
+
rmSync(join(parent, archive), { force: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("candidate contributor root accepts canonical Windows Git path spelling", { skip: process.platform !== "win32" }, (t) => {
|
|
239
|
+
const contributorRoot = repository(t);
|
|
240
|
+
let useCanonicalSpelling = false;
|
|
241
|
+
const registry = new CandidateViewRegistry((file, arguments_, options) => {
|
|
242
|
+
const output = execFileSync(file, arguments_, options);
|
|
243
|
+
if (!useCanonicalSpelling || typeof output !== "string" || arguments_[0] !== "rev-parse") return output;
|
|
244
|
+
if (arguments_[1] === "--show-toplevel") return output.toUpperCase();
|
|
245
|
+
if (arguments_[1] === "--git-common-dir") return resolve(contributorRoot, output).toUpperCase();
|
|
246
|
+
return output;
|
|
247
|
+
}, "win32");
|
|
248
|
+
mockWindowsAcl(t);
|
|
249
|
+
const view = registry.create({ contributorRoot });
|
|
250
|
+
try {
|
|
251
|
+
useCanonicalSpelling = true;
|
|
252
|
+
assert.doesNotThrow(() => view.verify());
|
|
253
|
+
} finally {
|
|
254
|
+
view.cleanup();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("private candidate owner ignores a spoofed SystemRoot when invoking Windows ACL tools", { skip: process.platform !== "win32" }, (t) => {
|
|
259
|
+
const originalSystemRoot = process.env.SystemRoot;
|
|
260
|
+
const spoofedSystemRoot = join(tmpdir(), "gentle-pi-spoofed-SystemRoot");
|
|
261
|
+
process.env.SystemRoot = spoofedSystemRoot;
|
|
262
|
+
t.after(() => {
|
|
263
|
+
if (originalSystemRoot === undefined) delete process.env.SystemRoot;
|
|
264
|
+
else process.env.SystemRoot = originalSystemRoot;
|
|
265
|
+
});
|
|
266
|
+
const execFile = childProcess.execFileSync;
|
|
267
|
+
const aclExecutables: string[] = [];
|
|
268
|
+
t.mock.method(childProcess, "execFileSync", (file, arguments_, options) => {
|
|
269
|
+
if (typeof file === "string" && /\\(?:whoami|icacls)\.exe$/i.test(file)) {
|
|
270
|
+
aclExecutables.push(file);
|
|
271
|
+
assert.equal(file.toLowerCase().startsWith(spoofedSystemRoot.toLowerCase()), false);
|
|
272
|
+
}
|
|
273
|
+
return execFile(file, arguments_, options);
|
|
274
|
+
});
|
|
275
|
+
syncBuiltinESMExports();
|
|
276
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
277
|
+
const view = new CandidateViewRegistry().create({ contributorRoot: repository(t) });
|
|
278
|
+
try {
|
|
279
|
+
view.verify();
|
|
280
|
+
assert.ok(aclExecutables.length > 0);
|
|
281
|
+
} finally {
|
|
282
|
+
view.cleanup();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("private candidate owner reads back a restrictive Windows DACL before worktree materialization", { skip: process.platform !== "win32" }, (t) => {
|
|
287
|
+
const system = process.env.SystemRoot!;
|
|
288
|
+
const view = new CandidateViewRegistry().create({ contributorRoot: repository(t) });
|
|
289
|
+
try {
|
|
290
|
+
const parent = join(view.root, ".."), archive = ".candidate-owner-acl.txt";
|
|
291
|
+
execFileSync(`${system}\\System32\\icacls.exe`, [parent, "/save", archive, "/c"], { cwd: parent, encoding: "utf8" });
|
|
292
|
+
const sddl = readFileSync(join(parent, archive), "utf16le");
|
|
293
|
+
rmSync(join(parent, archive));
|
|
294
|
+
const dacl = sddl.match(/D:[^\r\n]+/)?.[0];
|
|
295
|
+
assert.ok(dacl);
|
|
296
|
+
assert.doesNotThrow(() => view.verify());
|
|
297
|
+
} finally {
|
|
298
|
+
view.cleanup();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
function publicationFailure(t: test.TestContext, phase: "write" | "fsync" | "directory"): void {
|
|
303
|
+
mockWindowsAcl(t);
|
|
304
|
+
if (phase === "directory") {
|
|
305
|
+
const descriptor = Object.getOwnPropertyDescriptor(process, "platform")!;
|
|
306
|
+
Object.defineProperty(process, "platform", { configurable: true, value: "linux" });
|
|
307
|
+
t.after(() => Object.defineProperty(process, "platform", descriptor));
|
|
308
|
+
}
|
|
309
|
+
const cwd = repository(t), parent = join(cwd, ".git", "gentle-ai", "candidate-views");
|
|
310
|
+
let adds = 0;
|
|
311
|
+
if (phase === "write") t.mock.method(fs, "writeFileSync", () => { throw new Error("fixture marker write failure"); });
|
|
312
|
+
if (phase === "fsync") t.mock.method(fs, "fsyncSync", () => { throw new Error("fixture marker fsync failure"); });
|
|
313
|
+
if (phase === "directory") {
|
|
314
|
+
const fsync = fs.fsyncSync;
|
|
315
|
+
let syncs = 0;
|
|
316
|
+
t.mock.method(fs, "fsyncSync", (fd: number) => {
|
|
317
|
+
if (++syncs === 1) return fsync(fd);
|
|
318
|
+
throw new Error("fixture marker directory sync failure");
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
syncBuiltinESMExports();
|
|
322
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
323
|
+
const registry = new CandidateViewRegistry((file, args, options) => {
|
|
324
|
+
if (args[0] === "worktree" && args[1] === "add") adds++;
|
|
325
|
+
return execFileSync(file, args, options);
|
|
326
|
+
}, "win32");
|
|
327
|
+
assert.throws(
|
|
328
|
+
() => registry.create({ contributorRoot: cwd }),
|
|
329
|
+
(error: unknown) => error instanceof CandidateViewError && error.reason === "candidate-owner-preparation-failed",
|
|
330
|
+
);
|
|
331
|
+
assert.equal(adds, 0);
|
|
332
|
+
assert.deepEqual(readdirSync(parent).filter((name) => name.endsWith(".owner.json") || name.endsWith(".reaper-lock")), []);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const [phase, label] of [["write", "marker write"], ["fsync", "marker fsync"], ["directory", "post-marker directory sync"]] as const) {
|
|
336
|
+
test(`candidate owner publication rolls back after ${label} failure`, (t) => publicationFailure(t, phase));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
test("candidate owner publication never removes a replaced marker", (t) => {
|
|
340
|
+
mockWindowsAcl(t);
|
|
341
|
+
const cwd = repository(t), parent = join(cwd, ".git", "gentle-ai", "candidate-views");
|
|
342
|
+
const write = fs.writeFileSync;
|
|
343
|
+
t.mock.method(fs, "fsyncSync", () => {
|
|
344
|
+
const marker = readdirSync(parent).find((name) => name.endsWith(".owner.json"));
|
|
345
|
+
assert.ok(marker);
|
|
346
|
+
renameSync(join(parent, marker!), join(parent, `${marker}.saved`));
|
|
347
|
+
write(join(parent, marker!), "replacement", { mode: 0o600 });
|
|
348
|
+
throw new Error("fixture marker fsync failure after replacement");
|
|
349
|
+
});
|
|
350
|
+
syncBuiltinESMExports();
|
|
351
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
352
|
+
let adds = 0;
|
|
353
|
+
const registry = new CandidateViewRegistry((file, args, options) => {
|
|
354
|
+
if (args[0] === "worktree" && args[1] === "add") adds++;
|
|
355
|
+
return execFileSync(file, args, options);
|
|
356
|
+
}, "win32");
|
|
357
|
+
assert.throws(() => registry.create({ contributorRoot: cwd }), CandidateViewError);
|
|
358
|
+
assert.equal(adds, 0);
|
|
359
|
+
const marker = readdirSync(parent).find((name) => name.endsWith(".owner.json"));
|
|
360
|
+
assert.ok(marker);
|
|
361
|
+
assert.equal(readFileSync(join(parent, marker!), "utf8"), "replacement");
|
|
362
|
+
assert.equal(readdirSync(parent).some((name) => name.endsWith(".reaper-lock")), false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("candidate owner rejects an absent POSIX getuid before worktree creation", { skip: process.platform === "win32" }, (t) => {
|
|
366
|
+
const originalGetuid = process.getuid;
|
|
367
|
+
Object.defineProperty(process, "getuid", { configurable: true, value: undefined });
|
|
368
|
+
t.after(() => Object.defineProperty(process, "getuid", { configurable: true, value: originalGetuid }));
|
|
369
|
+
assert.throws(
|
|
370
|
+
() => new CandidateViewRegistry().create({ contributorRoot: repository(t) }),
|
|
371
|
+
(error: unknown) => error instanceof CandidateViewError && error.reason === "candidate-owner-preparation-failed",
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("candidate owner rejects a POSIX UID mismatch before worktree creation", { skip: process.platform === "win32" }, (t) => {
|
|
376
|
+
const uid = process.getuid?.();
|
|
377
|
+
assert.equal(typeof uid, "number");
|
|
378
|
+
const originalGetuid = process.getuid;
|
|
379
|
+
Object.defineProperty(process, "getuid", { configurable: true, value: () => uid! + 1 });
|
|
380
|
+
t.after(() => Object.defineProperty(process, "getuid", { configurable: true, value: originalGetuid }));
|
|
381
|
+
assert.throws(
|
|
382
|
+
() => new CandidateViewRegistry().create({ contributorRoot: repository(t) }),
|
|
383
|
+
(error: unknown) => error instanceof CandidateViewError && error.reason === "candidate-owner-preparation-failed",
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
92
387
|
test("marked registered definitively dead candidate is reclaimed before another materialization", (t) => {
|
|
93
388
|
const { cwd, view, marker } = orphanFixture(t);
|
|
94
389
|
mockOwnerProbe(t, "ESRCH");
|
|
@@ -206,7 +501,10 @@ test("owner fsync failure prevents worktree registration", (t) => {
|
|
|
206
501
|
if (args[0] === "worktree" && args[1] === "add") adds++;
|
|
207
502
|
return execFileSync(file, args, options);
|
|
208
503
|
});
|
|
209
|
-
assert.throws(
|
|
504
|
+
assert.throws(
|
|
505
|
+
() => registry.create({ contributorRoot: cwd }),
|
|
506
|
+
(error: unknown) => error instanceof CandidateViewError && error.reason === "candidate-owner-preparation-failed" && error.cause instanceof Error && /fsync/.test(error.cause.message),
|
|
507
|
+
);
|
|
210
508
|
assert.equal(adds, 0);
|
|
211
509
|
});
|
|
212
510
|
|
|
@@ -1066,7 +1364,7 @@ test("candidate executable-mode validation accepts a readonly Git executable on
|
|
|
1066
1364
|
});
|
|
1067
1365
|
|
|
1068
1366
|
test("candidate registry forwards explicit Windows mode validation to view verification", (t) => {
|
|
1069
|
-
|
|
1367
|
+
mockWindowsAcl(t);
|
|
1070
1368
|
const contributorRoot = repository(t);
|
|
1071
1369
|
writeFileSync(join(contributorRoot, "unchanged-executable.sh"), "#!/bin/sh\necho base\n");
|
|
1072
1370
|
git(contributorRoot, "add", "unchanged-executable.sh");
|