@junghanacs/entwurf 0.12.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/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-pi-preflight — deterministic gate for 0.11 Stage 0 (2): the controlled-
|
|
3
|
+
* launch trust decision. Pure decision + a real temp agentDir; no backend, no
|
|
4
|
+
* network, no hook. Safe in the `pnpm check` static floor.
|
|
5
|
+
*
|
|
6
|
+
* Synthetic fixture = pi's OWN `ProjectTrustStore` pointed at a temp agentDir:
|
|
7
|
+
* we seed trust with `store.set(cwd, …)` and `preflight()` reads it back through
|
|
8
|
+
* the same store, so the saved-trust path is checked against pi's real function
|
|
9
|
+
* (same process — trivial, frozen ledger). `PI_CODING_AGENT_DIR` / the injected
|
|
10
|
+
* `agentDir` keep this off the operator's real `~/.pi/agent/trust.json`.
|
|
11
|
+
*
|
|
12
|
+
* Proves frozen decision 8 precedence end to end:
|
|
13
|
+
* saved false > saved true > prefix match > no-trust-inputs > fail-fast
|
|
14
|
+
* plus decision 7's separator boundary (`/repos` ≠ `/repos-sibling`), `~`
|
|
15
|
+
* expansion of operator roots, and the rich evidence the fact surface consumes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { formatPreflightDenial, preflight } from "../pi-extensions/lib/entwurf-preflight.ts";
|
|
24
|
+
|
|
25
|
+
let passed = 0;
|
|
26
|
+
function ok(label: string, cond: boolean): void {
|
|
27
|
+
assert.ok(cond, label);
|
|
28
|
+
console.log(` ok ${label}`);
|
|
29
|
+
passed++;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tmpRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "psa-preflight-")));
|
|
33
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
34
|
+
const prefixRoot = path.join(tmpRoot, "repos");
|
|
35
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
36
|
+
fs.mkdirSync(prefixRoot, { recursive: true });
|
|
37
|
+
|
|
38
|
+
// Seed a trust-gated project input. We use a `.pi/settings.json` config file,
|
|
39
|
+
// NOT an AGENTS.md: pi 0.79.x dropped AGENTS.md/CLAUDE.md from
|
|
40
|
+
// hasTrustRequiringProjectResources (they are now always-loaded context files,
|
|
41
|
+
// not trust-gated inputs). `.pi` is cwd-scoped, so each fixture is an
|
|
42
|
+
// independent trust input with no walk-to-root bleed; current pi requires one of
|
|
43
|
+
// the trust-requiring entries under `.pi`, not a bare directory.
|
|
44
|
+
function seedTrustInput(dir: string): void {
|
|
45
|
+
const piDir = path.join(dir, ".pi");
|
|
46
|
+
fs.mkdirSync(piDir, { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(piDir, "settings.json"), "{}\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A real cwd must exist for ProjectTrustStore's realpathSync canonicalization
|
|
51
|
+
// and for hasTrustRequiringProjectResources' filesystem walk.
|
|
52
|
+
function mkCwd(name: string, withTrustInput = false): string {
|
|
53
|
+
const dir = path.join(prefixRoot, name);
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
if (withTrustInput) seedTrustInput(dir);
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const store = new ProjectTrustStore(agentDir);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// 1. saved false beats a prefix match (explicit distrust wins — the load-
|
|
63
|
+
// bearing half of decision 8). Evidence carries the raw store value.
|
|
64
|
+
const cwd1 = mkCwd("saved-false", true);
|
|
65
|
+
store.set(cwd1, false);
|
|
66
|
+
const d1 = preflight({ cwd: cwd1, agentDir, prefixRoots: [prefixRoot] });
|
|
67
|
+
ok(
|
|
68
|
+
"saved false → deny/saved-false (even under a prefix root)",
|
|
69
|
+
d1.kind === "deny" && d1.reason === "saved-false" && d1.trustStoreDecision === false && d1.launchArgs.length === 0,
|
|
70
|
+
);
|
|
71
|
+
// F5a evidence: a DIRECT decision on the cwd carries entryPath === the cwd
|
|
72
|
+
// (pi-canonical === our normalizePath here, since tmpRoot is a realpath) and
|
|
73
|
+
// is NOT inherited. This is also the entryPath axis assertion — if pi's
|
|
74
|
+
// canonicalizePath diverged from our normalizePath on this cwd, entryPath
|
|
75
|
+
// would not equal cwd1 and this fails loud (NEXT: symlink edge caught here).
|
|
76
|
+
ok(
|
|
77
|
+
"direct saved-false: entryPath === cwd, not inherited",
|
|
78
|
+
d1.trustStoreEntryPath === cwd1 && d1.trustStoreInherited === false,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// 2. saved true → approve, launchArgs carries the internal --approve.
|
|
82
|
+
const cwd2 = mkCwd("saved-true", true);
|
|
83
|
+
store.set(cwd2, true);
|
|
84
|
+
const d2 = preflight({ cwd: cwd2, agentDir });
|
|
85
|
+
ok(
|
|
86
|
+
"saved true → approve/saved-true + launchArgs=['--approve']",
|
|
87
|
+
d2.kind === "approve" &&
|
|
88
|
+
d2.reason === "saved-true" &&
|
|
89
|
+
d2.trustStoreDecision === true &&
|
|
90
|
+
d2.launchArgs.length === 1 &&
|
|
91
|
+
d2.launchArgs[0] === "--approve",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// 3. undecided + prefix match → approve; evidence names the matched root AND
|
|
95
|
+
// still reports hasTrustInputs (the fact surface explains what it may load).
|
|
96
|
+
const cwd3 = mkCwd("prefix-hit", true); // has a trust input, yet prefix wins over fail-fast
|
|
97
|
+
const d3 = preflight({ cwd: cwd3, agentDir, prefixRoots: [prefixRoot] });
|
|
98
|
+
ok(
|
|
99
|
+
"null + prefix match → approve/prefix-match + matchedPrefixRoot + hasTrustInputs",
|
|
100
|
+
d3.kind === "approve" &&
|
|
101
|
+
d3.reason === "prefix-match" &&
|
|
102
|
+
d3.matchedPrefixRoot === prefixRoot &&
|
|
103
|
+
d3.hasTrustInputs === true &&
|
|
104
|
+
d3.launchArgs[0] === "--approve",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// 4. undecided + no trust inputs + no prefix → trusted-no-arg, no launch arg.
|
|
108
|
+
const cwd4 = path.join(tmpRoot, "no-inputs"); // OUTSIDE prefixRoot, no trust input
|
|
109
|
+
fs.mkdirSync(cwd4, { recursive: true });
|
|
110
|
+
const d4 = preflight({ cwd: cwd4, agentDir, prefixRoots: [prefixRoot] });
|
|
111
|
+
ok(
|
|
112
|
+
"null + no trust inputs → trusted-no-arg + launchArgs=[] + hasTrustInputs=false",
|
|
113
|
+
d4.kind === "trusted-no-arg" &&
|
|
114
|
+
d4.reason === "no-trust-inputs" &&
|
|
115
|
+
d4.hasTrustInputs === false &&
|
|
116
|
+
d4.launchArgs.length === 0,
|
|
117
|
+
);
|
|
118
|
+
// F5a evidence: no store decision (neither cwd nor any ancestor) ⇒ no entry
|
|
119
|
+
// path and not inherited. The deny/handler layers must see "nothing decided".
|
|
120
|
+
ok(
|
|
121
|
+
"no decision → entryPath undefined + not inherited",
|
|
122
|
+
d4.trustStoreEntryPath === undefined && d4.trustStoreInherited === false,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 5. undecided + trust inputs + no prefix → fail-fast.
|
|
126
|
+
const cwd5 = path.join(tmpRoot, "lonely-inputs"); // OUTSIDE prefixRoot
|
|
127
|
+
fs.mkdirSync(cwd5, { recursive: true });
|
|
128
|
+
seedTrustInput(cwd5);
|
|
129
|
+
const d5 = preflight({ cwd: cwd5, agentDir, prefixRoots: [prefixRoot] });
|
|
130
|
+
ok(
|
|
131
|
+
"null + trust inputs + no prefix → deny/fail-fast",
|
|
132
|
+
d5.kind === "deny" && d5.reason === "fail-fast" && d5.hasTrustInputs === true && d5.launchArgs.length === 0,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// 6. empty roots ⇒ no prefix promotion (frozen decision 7: no package default).
|
|
136
|
+
const cwd6 = mkCwd("would-match-but-no-roots", true);
|
|
137
|
+
const d6 = preflight({ cwd: cwd6, agentDir, prefixRoots: [] });
|
|
138
|
+
ok(
|
|
139
|
+
"undecided + inputs + EMPTY roots → fail-fast (no package-default prefix)",
|
|
140
|
+
d6.kind === "deny" && d6.reason === "fail-fast" && d6.matchedPrefixRoot === undefined,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// 7. separator boundary: a sibling sharing a string prefix must NOT match.
|
|
144
|
+
// root=<tmp>/repos, cwd=<tmp>/repos-sibling — bare startsWith would wrongly
|
|
145
|
+
// match; canonical + sep boundary must not.
|
|
146
|
+
const sibling = path.join(tmpRoot, "repos-sibling");
|
|
147
|
+
fs.mkdirSync(sibling, { recursive: true });
|
|
148
|
+
seedTrustInput(sibling);
|
|
149
|
+
const d7 = preflight({ cwd: sibling, agentDir, prefixRoots: [prefixRoot] });
|
|
150
|
+
ok(
|
|
151
|
+
"prefix `/repos` does NOT match sibling `/repos-sibling` (sep boundary)",
|
|
152
|
+
d7.kind === "deny" && d7.reason === "fail-fast" && d7.matchedPrefixRoot === undefined,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// 8. the root itself (cwd === root) matches, and canonicalCwd is the realpath.
|
|
156
|
+
const dRoot = preflight({ cwd: prefixRoot, agentDir, prefixRoots: [prefixRoot] });
|
|
157
|
+
ok(
|
|
158
|
+
"cwd === prefix root matches (boundary inclusive) + canonicalCwd is realpath",
|
|
159
|
+
dRoot.kind === "approve" && dRoot.canonicalCwd === prefixRoot,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// 9. `~`-relative operator root expands and matches (frozen decision 7 GLG
|
|
163
|
+
// default is `~/repos/gh` shaped). HOME → temp so we never touch the real
|
|
164
|
+
// home; os.homedir() reads $HOME on Linux.
|
|
165
|
+
const origHome = process.env.HOME;
|
|
166
|
+
process.env.HOME = tmpRoot;
|
|
167
|
+
try {
|
|
168
|
+
const tildeProj = path.join(tmpRoot, "repos", "gh", "proj");
|
|
169
|
+
fs.mkdirSync(tildeProj, { recursive: true });
|
|
170
|
+
seedTrustInput(tildeProj);
|
|
171
|
+
const dTilde = preflight({ cwd: tildeProj, agentDir, prefixRoots: ["~/repos"] });
|
|
172
|
+
ok(
|
|
173
|
+
"`~/repos` operator root expands and matches cwd under it",
|
|
174
|
+
dTilde.kind === "approve" && dTilde.reason === "prefix-match" && dTilde.matchedPrefixRoot === prefixRoot,
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
if (origHome === undefined) delete process.env.HOME;
|
|
178
|
+
else process.env.HOME = origHome;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 10-12. nearest-ancestor trust inheritance (pi 0.79.x). 0.79.0's store.get
|
|
182
|
+
// matched the cwd EXACTLY; 0.79.x getEntry/findNearestTrustEntry walks
|
|
183
|
+
// up to the closest ancestor carrying an explicit decision. preflight reads
|
|
184
|
+
// pi's store directly (frozen decision 9), so this propagation flows
|
|
185
|
+
// through verbatim — a saved decision on a PARENT now decides a child that
|
|
186
|
+
// has no decision of its own. This is the production half of frozen
|
|
187
|
+
// decision 8: an operator distrust on `~/repos/gh` reaches every repo under
|
|
188
|
+
// it. These assertions FAIL on 0.79.0 (parent set, child get → null) and
|
|
189
|
+
// are intentionally gated behind the 0.79.3 floor (committed with the bump).
|
|
190
|
+
|
|
191
|
+
// 10. inherited distrust beats a prefix match: parent=false, child under both
|
|
192
|
+
// the parent AND a prefix root → the inherited saved-false still denies
|
|
193
|
+
// (saved distrust is the strongest rung; a prefix only promotes null).
|
|
194
|
+
const inheritParent = mkCwd("inherit-deny", true);
|
|
195
|
+
const inheritChild = path.join(inheritParent, "nested", "child");
|
|
196
|
+
fs.mkdirSync(inheritChild, { recursive: true });
|
|
197
|
+
store.set(inheritParent, false);
|
|
198
|
+
const dInheritDeny = preflight({ cwd: inheritChild, agentDir, prefixRoots: [prefixRoot] });
|
|
199
|
+
ok(
|
|
200
|
+
"child inherits parent's saved-false (nearest ancestor) → deny, even under a prefix root",
|
|
201
|
+
dInheritDeny.kind === "deny" && dInheritDeny.reason === "saved-false" && dInheritDeny.trustStoreDecision === false,
|
|
202
|
+
);
|
|
203
|
+
// F5a evidence: an INHERITED decision carries entryPath === the ANCESTOR that
|
|
204
|
+
// holds it (not the cwd) and is flagged inherited. This is the raw material
|
|
205
|
+
// N3b's deny message turns into "inheritedFrom <parent> — open an interactive
|
|
206
|
+
// pi at <cwd> to override".
|
|
207
|
+
ok(
|
|
208
|
+
"inherited saved-false: entryPath === parent (ancestor), inherited === true",
|
|
209
|
+
dInheritDeny.trustStoreEntryPath === inheritParent && dInheritDeny.trustStoreInherited === true,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// 11. inherited trust: parent=true, child with no own decision → approve via
|
|
213
|
+
// saved-true (reason is saved-true, NOT prefix-match — the store wins first).
|
|
214
|
+
const trustParent = mkCwd("inherit-approve", true);
|
|
215
|
+
const trustChild = path.join(trustParent, "nested", "child");
|
|
216
|
+
fs.mkdirSync(trustChild, { recursive: true });
|
|
217
|
+
store.set(trustParent, true);
|
|
218
|
+
const dInheritApprove = preflight({ cwd: trustChild, agentDir });
|
|
219
|
+
ok(
|
|
220
|
+
"child inherits parent's saved-true (nearest ancestor) → approve/saved-true + --approve",
|
|
221
|
+
dInheritApprove.kind === "approve" &&
|
|
222
|
+
dInheritApprove.reason === "saved-true" &&
|
|
223
|
+
dInheritApprove.trustStoreDecision === true &&
|
|
224
|
+
dInheritApprove.launchArgs[0] === "--approve",
|
|
225
|
+
);
|
|
226
|
+
ok(
|
|
227
|
+
"inherited saved-true: entryPath === parent (ancestor), inherited === true",
|
|
228
|
+
dInheritApprove.trustStoreEntryPath === trustParent && dInheritApprove.trustStoreInherited === true,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// 12. nearest wins: the child's OWN false overrides an ancestor true (the walk
|
|
232
|
+
// stops at the closest decision, it does not keep climbing past it).
|
|
233
|
+
const nearestChild = path.join(trustParent, "distrusted-leaf");
|
|
234
|
+
fs.mkdirSync(nearestChild, { recursive: true });
|
|
235
|
+
store.set(nearestChild, false);
|
|
236
|
+
const dNearest = preflight({ cwd: nearestChild, agentDir });
|
|
237
|
+
ok(
|
|
238
|
+
"child's own saved-false overrides an ancestor saved-true (nearest entry wins)",
|
|
239
|
+
dNearest.kind === "deny" && dNearest.reason === "saved-false" && dNearest.trustStoreDecision === false,
|
|
240
|
+
);
|
|
241
|
+
ok(
|
|
242
|
+
"nearest direct false (over ancestor true): entryPath === cwd, not inherited",
|
|
243
|
+
dNearest.trustStoreEntryPath === nearestChild && dNearest.trustStoreInherited === false,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// 13b. ESCAPE DIRECTION (F5a / Trust 2층 active-prompt). The mirror of #10:
|
|
247
|
+
// parent=false, but the child carries its OWN saved-true. nearest-entry
|
|
248
|
+
// wins, so the child approves DESPITE the inherited distrust. This is the
|
|
249
|
+
// exact post-state of the only escape from an inherited false — a human
|
|
250
|
+
// opens an interactive pi at the child cwd, the handler prompts, and yes →
|
|
251
|
+
// `{trusted:"yes", remember:true}` writes a direct child true. Once that
|
|
252
|
+
// entry exists, preflight must approve the child (entryPath === child, not
|
|
253
|
+
// inherited), proving the escape actually releases the launch.
|
|
254
|
+
const escapeParent = mkCwd("escape-parent", true);
|
|
255
|
+
const escapeChild = path.join(escapeParent, "trusted-leaf");
|
|
256
|
+
fs.mkdirSync(escapeChild, { recursive: true });
|
|
257
|
+
store.set(escapeParent, false);
|
|
258
|
+
store.set(escapeChild, true);
|
|
259
|
+
const dEscape = preflight({ cwd: escapeChild, agentDir });
|
|
260
|
+
ok(
|
|
261
|
+
"child's own saved-true beats an ancestor saved-false (escape direction) → approve",
|
|
262
|
+
dEscape.kind === "approve" &&
|
|
263
|
+
dEscape.reason === "saved-true" &&
|
|
264
|
+
dEscape.trustStoreEntryPath === escapeChild &&
|
|
265
|
+
dEscape.trustStoreInherited === false &&
|
|
266
|
+
dEscape.launchArgs[0] === "--approve",
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// N3b. deny-message formatter — a PURE formatter over deny evidence (no
|
|
270
|
+
// launcher wiring; that is bucket B). The inherited-false message MUST
|
|
271
|
+
// name the inheritedFrom source AND the active-prompt remedy; the other
|
|
272
|
+
// two deny shapes must not leak an ancestor they don't have.
|
|
273
|
+
if (dInheritDeny.kind === "deny") {
|
|
274
|
+
const msg = formatPreflightDenial(dInheritDeny);
|
|
275
|
+
ok(
|
|
276
|
+
"N3b: inherited-false deny names inheritedFrom (ancestor) + interactive-pi remedy + cwd",
|
|
277
|
+
msg.includes(inheritParent) && msg.includes("interactive pi") && msg.includes(inheritChild),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (d1.kind === "deny") {
|
|
281
|
+
const msg = formatPreflightDenial(d1);
|
|
282
|
+
// Direct distrust: explains "explicitly distrusted" + remedy, and does NOT
|
|
283
|
+
// invent an ancestor (cwd1 has no inherited source).
|
|
284
|
+
ok(
|
|
285
|
+
"N3b: direct-false deny says explicitly distrusted + remedy, no phantom ancestor",
|
|
286
|
+
msg.includes("explicitly distrusted") && msg.includes("interactive pi") && msg.includes(cwd1),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (d5.kind === "deny") {
|
|
290
|
+
const msg = formatPreflightDenial(d5);
|
|
291
|
+
ok(
|
|
292
|
+
"N3b: fail-fast deny says untrusted + prefix-root-or-interactive remedy",
|
|
293
|
+
msg.includes("untrusted") && msg.includes("prefix") && msg.includes("interactive pi"),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 13. isolation: every set above landed in the temp store; the real operator
|
|
298
|
+
// trust.json was never opened.
|
|
299
|
+
ok("temp agentDir holds the trust file (real ~/.pi untouched)", fs.existsSync(path.join(agentDir, "trust.json")));
|
|
300
|
+
} finally {
|
|
301
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(`[check-pi-preflight] ${passed} assertions ok`);
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-project-trust-handler — deterministic gate for 0.11 Stage 0 (Trust 2층
|
|
3
|
+
* active-prompt escape). Drives the pure `decideProjectTrust` matrix with a fake
|
|
4
|
+
* prompt and the `createProjectTrustHandler` adapter with a fake `ctx.ui.select`,
|
|
5
|
+
* so NO real pi UI opens. Real outcomes come from `preflight` over a temp
|
|
6
|
+
* agentDir (same isolation as check-pi-preflight). No backend, no network.
|
|
7
|
+
*
|
|
8
|
+
* Proves the GLG 6 review points:
|
|
9
|
+
* ① single writer — the handler returns `{yes, remember:true}` but NEVER calls
|
|
10
|
+
* store.set; the end-to-end case asserts trust.json gains no child key.
|
|
11
|
+
* ② no undefined — every branch returns a concrete `{trusted, …}`.
|
|
12
|
+
* ③ non-interactive (print / rpc) never prompts → undecided.
|
|
13
|
+
* ④ ctx.ui injection — fake prompt drives the matrix; the escape prompt title
|
|
14
|
+
* reuses F5a evidence (names the inherited source).
|
|
15
|
+
* ⑤ inherited distrust escape: interactive + "trust-here" → `{yes, remember:true}`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { preflight } from "../pi-extensions/lib/entwurf-preflight.ts";
|
|
24
|
+
import {
|
|
25
|
+
type ActivePrompt,
|
|
26
|
+
type ActivePromptChoice,
|
|
27
|
+
createProjectTrustHandler,
|
|
28
|
+
decideProjectTrust,
|
|
29
|
+
KEEP_DISTRUSTED_LABEL,
|
|
30
|
+
TRUST_HERE_LABEL,
|
|
31
|
+
} from "../pi-extensions/lib/project-trust-handler.ts";
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
function ok(label: string, cond: boolean): void {
|
|
35
|
+
assert.ok(cond, label);
|
|
36
|
+
console.log(` ok ${label}`);
|
|
37
|
+
passed++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fakePrompt(answer: ActivePromptChoice): { prompt: ActivePrompt; calls: () => number } {
|
|
41
|
+
let calls = 0;
|
|
42
|
+
const prompt: ActivePrompt = async () => {
|
|
43
|
+
calls += 1;
|
|
44
|
+
return answer;
|
|
45
|
+
};
|
|
46
|
+
return { prompt, calls: () => calls };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const TUI = { hasUI: true, mode: "tui" as const };
|
|
50
|
+
const PRINT = { hasUI: false, mode: "print" as const };
|
|
51
|
+
const RPC = { hasUI: true, mode: "rpc" as const }; // hasUI but not a human at a TUI
|
|
52
|
+
|
|
53
|
+
const tmpRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "psa-pth-")));
|
|
54
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
55
|
+
const reposRoot = path.join(tmpRoot, "repos");
|
|
56
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
57
|
+
fs.mkdirSync(reposRoot, { recursive: true });
|
|
58
|
+
const store = new ProjectTrustStore(agentDir);
|
|
59
|
+
|
|
60
|
+
function mkdir(p: string): string {
|
|
61
|
+
fs.mkdirSync(p, { recursive: true });
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
function seedTrustInput(dir: string): string {
|
|
65
|
+
const piDir = path.join(dir, ".pi");
|
|
66
|
+
fs.mkdirSync(piDir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(path.join(piDir, "settings.json"), "{}\n");
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Build REAL outcomes via preflight (exact evidence shape), then decide.
|
|
73
|
+
// approve (saved-true)
|
|
74
|
+
const cwdApprove = seedTrustInput(mkdir(path.join(reposRoot, "approve")));
|
|
75
|
+
store.set(cwdApprove, true);
|
|
76
|
+
const oApprove = preflight({ cwd: cwdApprove, agentDir });
|
|
77
|
+
// trusted-no-arg (no trust input)
|
|
78
|
+
const cwdNoArg = mkdir(path.join(reposRoot, "no-inputs"));
|
|
79
|
+
const oNoArg = preflight({ cwd: cwdNoArg, agentDir });
|
|
80
|
+
// direct distrust
|
|
81
|
+
const cwdDirect = seedTrustInput(mkdir(path.join(reposRoot, "direct-false")));
|
|
82
|
+
store.set(cwdDirect, false);
|
|
83
|
+
const oDirect = preflight({ cwd: cwdDirect, agentDir });
|
|
84
|
+
// inherited distrust (parent false, child has its OWN trust input but no direct
|
|
85
|
+
// decision). The child trust input is load-bearing for THIS gate: pi's
|
|
86
|
+
// resolveProjectTrusted returns true early when !hasTrustRequiringProjectResources(cwd), so
|
|
87
|
+
// WITHOUT a child trust input the project_trust event never fires and the escape
|
|
88
|
+
// prompt is unreachable in the real pi path. With it, the event fires and the
|
|
89
|
+
// inherited parent false is exactly what the escape overrides (GPT review).
|
|
90
|
+
const inheritParent = seedTrustInput(mkdir(path.join(reposRoot, "inherit")));
|
|
91
|
+
store.set(inheritParent, false);
|
|
92
|
+
const inheritChild = seedTrustInput(mkdir(path.join(inheritParent, "nested", "child")));
|
|
93
|
+
const oInherit = preflight({ cwd: inheritChild, agentDir });
|
|
94
|
+
// fail-fast (trust input, no prefix root)
|
|
95
|
+
const cwdFailFast = seedTrustInput(mkdir(path.join(reposRoot, "fail-fast")));
|
|
96
|
+
const oFailFast = preflight({ cwd: cwdFailFast, agentDir });
|
|
97
|
+
|
|
98
|
+
// Sanity: outcomes are the shapes we expect to decide over.
|
|
99
|
+
ok(
|
|
100
|
+
"setup: outcome kinds as expected",
|
|
101
|
+
oApprove.kind === "approve" &&
|
|
102
|
+
oNoArg.kind === "trusted-no-arg" &&
|
|
103
|
+
oDirect.kind === "deny" &&
|
|
104
|
+
oInherit.kind === "deny" &&
|
|
105
|
+
oFailFast.kind === "deny",
|
|
106
|
+
);
|
|
107
|
+
ok(
|
|
108
|
+
"setup: inherited fixture is event-reachable (parent false + child trust input)",
|
|
109
|
+
oInherit.kind === "deny" &&
|
|
110
|
+
oInherit.reason === "saved-false" &&
|
|
111
|
+
oInherit.trustStoreInherited === true &&
|
|
112
|
+
oInherit.hasTrustInputs === true && // pi fires project_trust only when this is true
|
|
113
|
+
oDirect.kind === "deny" &&
|
|
114
|
+
oDirect.trustStoreInherited === false,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// ── decideProjectTrust matrix ──────────────────────────────────────────
|
|
118
|
+
{
|
|
119
|
+
const fp = fakePrompt("trust-here");
|
|
120
|
+
const r = await decideProjectTrust(oApprove, TUI, fp.prompt);
|
|
121
|
+
ok("approve → {yes, remember:false}, no prompt", r.trusted === "yes" && r.remember === false && fp.calls() === 0);
|
|
122
|
+
}
|
|
123
|
+
{
|
|
124
|
+
const fp = fakePrompt("trust-here");
|
|
125
|
+
const r = await decideProjectTrust(oNoArg, TUI, fp.prompt);
|
|
126
|
+
ok(
|
|
127
|
+
"trusted-no-arg → {yes, remember:false}, no prompt",
|
|
128
|
+
r.trusted === "yes" && r.remember === false && fp.calls() === 0,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
{
|
|
132
|
+
const fp = fakePrompt("trust-here");
|
|
133
|
+
const r = await decideProjectTrust(oDirect, TUI, fp.prompt);
|
|
134
|
+
ok(
|
|
135
|
+
"direct distrust → {no, remember:false}, no prompt (already stored)",
|
|
136
|
+
r.trusted === "no" && r.remember === false && fp.calls() === 0,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
{
|
|
140
|
+
// ⑤ THE ESCAPE: inherited false + interactive + trust-here → {yes, remember:true}
|
|
141
|
+
const fp = fakePrompt("trust-here");
|
|
142
|
+
const r = await decideProjectTrust(oInherit, TUI, fp.prompt);
|
|
143
|
+
ok(
|
|
144
|
+
"inherited false + interactive + trust-here → {yes, remember:true} (escape)",
|
|
145
|
+
r.trusted === "yes" && r.remember === true && fp.calls() === 1,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
{
|
|
149
|
+
const fp = fakePrompt("no");
|
|
150
|
+
const r = await decideProjectTrust(oInherit, TUI, fp.prompt);
|
|
151
|
+
ok(
|
|
152
|
+
"inherited false + interactive + no → {no, remember:false} (R3a: no child false)",
|
|
153
|
+
r.trusted === "no" && r.remember === false && fp.calls() === 1,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
{
|
|
157
|
+
const fp = fakePrompt("cancel");
|
|
158
|
+
const r = await decideProjectTrust(oInherit, TUI, fp.prompt);
|
|
159
|
+
ok(
|
|
160
|
+
"inherited false + interactive + cancel/ESC → {undecided} (defer = safe deny)",
|
|
161
|
+
r.trusted === "undecided" && fp.calls() === 1,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
{
|
|
165
|
+
// ③ non-interactive: pi -p (print) never prompts
|
|
166
|
+
const fp = fakePrompt("trust-here");
|
|
167
|
+
const r = await decideProjectTrust(oInherit, PRINT, fp.prompt);
|
|
168
|
+
ok("inherited false + print (pi -p) → {undecided}, NO prompt", r.trusted === "undecided" && fp.calls() === 0);
|
|
169
|
+
}
|
|
170
|
+
{
|
|
171
|
+
// ③ hasUI but not a TUI (rpc) — still no active prompt
|
|
172
|
+
const fp = fakePrompt("trust-here");
|
|
173
|
+
const r = await decideProjectTrust(oInherit, RPC, fp.prompt);
|
|
174
|
+
ok(
|
|
175
|
+
"inherited false + rpc (hasUI, not tui) → {undecided}, NO prompt",
|
|
176
|
+
r.trusted === "undecided" && fp.calls() === 0,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
{
|
|
180
|
+
const fp = fakePrompt("trust-here");
|
|
181
|
+
const r = await decideProjectTrust(oFailFast, TUI, fp.prompt);
|
|
182
|
+
ok("fail-fast → {undecided}, no active prompt (defer to pi's own)", r.trusted === "undecided" && fp.calls() === 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ② no undefined: every outcome×ctx returns a concrete trusted field.
|
|
186
|
+
{
|
|
187
|
+
const all = [oApprove, oNoArg, oDirect, oInherit, oFailFast];
|
|
188
|
+
const ctxs = [TUI, PRINT, RPC];
|
|
189
|
+
let everReturned = 0;
|
|
190
|
+
let everUndefined = 0;
|
|
191
|
+
for (const o of all) {
|
|
192
|
+
for (const c of ctxs) {
|
|
193
|
+
const r = await decideProjectTrust(o, c, fakePrompt("cancel").prompt);
|
|
194
|
+
everReturned += 1;
|
|
195
|
+
if (r.trusted === undefined) everUndefined += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
ok("② every branch returns a concrete result (never undefined)", everReturned === 15 && everUndefined === 0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── adapter end-to-end (① single writer + ④ ctx.ui injection) ───────────
|
|
202
|
+
{
|
|
203
|
+
let selectTitle = "";
|
|
204
|
+
const handler = createProjectTrustHandler({ prefixRoots: [], agentDir });
|
|
205
|
+
const fakeCtx = {
|
|
206
|
+
cwd: inheritChild,
|
|
207
|
+
mode: "tui" as const,
|
|
208
|
+
hasUI: true,
|
|
209
|
+
ui: {
|
|
210
|
+
select: async (title: string, _options: string[]) => {
|
|
211
|
+
selectTitle = title;
|
|
212
|
+
return TRUST_HERE_LABEL;
|
|
213
|
+
},
|
|
214
|
+
confirm: async () => false,
|
|
215
|
+
input: async () => undefined,
|
|
216
|
+
notify: () => {},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
const r = await handler({ type: "project_trust", cwd: inheritChild }, fakeCtx);
|
|
220
|
+
ok(
|
|
221
|
+
"adapter: inherited child + select(trust-here) → {yes, remember:true}",
|
|
222
|
+
r.trusted === "yes" && r.remember === true,
|
|
223
|
+
);
|
|
224
|
+
ok(
|
|
225
|
+
"④ adapter prompt title reuses F5a evidence (names inherited source)",
|
|
226
|
+
selectTitle.includes(inheritParent) && selectTitle.includes("interactive pi"),
|
|
227
|
+
);
|
|
228
|
+
// ① single writer: the handler did NOT persist. Only pi's resolveProjectTrusted
|
|
229
|
+
// would, on remember:true. So trust.json still has parent=false and NO child key.
|
|
230
|
+
const fresh = new ProjectTrustStore(agentDir);
|
|
231
|
+
ok(
|
|
232
|
+
"① handler never wrote the child key (pi is the single writer)",
|
|
233
|
+
fresh.getEntry(inheritChild)?.path === inheritParent,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// label sanity: the no-label maps to "no", an unknown label maps to cancel.
|
|
238
|
+
{
|
|
239
|
+
let selected = "";
|
|
240
|
+
const handler = createProjectTrustHandler({ prefixRoots: [], agentDir });
|
|
241
|
+
const ctx = {
|
|
242
|
+
cwd: inheritChild,
|
|
243
|
+
mode: "tui" as const,
|
|
244
|
+
hasUI: true,
|
|
245
|
+
ui: {
|
|
246
|
+
select: async () => {
|
|
247
|
+
selected = KEEP_DISTRUSTED_LABEL;
|
|
248
|
+
return KEEP_DISTRUSTED_LABEL;
|
|
249
|
+
},
|
|
250
|
+
confirm: async () => false,
|
|
251
|
+
input: async () => undefined,
|
|
252
|
+
notify: () => {},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
const r = await handler({ type: "project_trust", cwd: inheritChild }, ctx);
|
|
256
|
+
ok(
|
|
257
|
+
"adapter: select(keep-distrusted) → {no, remember:false}",
|
|
258
|
+
r.trusted === "no" && r.remember === false && selected === KEEP_DISTRUSTED_LABEL,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
} finally {
|
|
262
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(`[check-project-trust-handler] ${passed} assertions ok`);
|