@junghanacs/entwurf 0.12.8 → 0.12.10
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 +134 -248
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +39 -1
- package/DELIVERY.md +5 -4
- package/README.md +148 -23
- package/VERIFY.md +3 -2
- package/demo/README.md +2 -2
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +24 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +168 -81
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +16 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +14 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +500 -54
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
- package/mcp/entwurf-bridge/src/index.ts +200 -94
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +81 -39
- package/pi-extensions/lib/acp/models.ts +12 -12
- package/pi-extensions/lib/acp/overlay.ts +5 -3
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +24 -19
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +22 -13
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +11 -3
- package/pi-extensions/lib/meta-sender-identity.ts +15 -5
- package/pi-extensions/lib/meta-session.ts +543 -58
- package/pi-extensions/meta-bridge-hook.ts +8 -2
- package/run.sh +78 -67
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/agy-imprint.ts +15 -1
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-acp-session-reuse.ts +1 -1
- package/scripts/check-acp-session-store.ts +3 -3
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-agy-sender-identity.ts +83 -0
- package/scripts/check-entwurf-control-rpc.ts +2 -2
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +16 -8
- package/scripts/check-entwurf-facts.ts +13 -13
- package/scripts/check-entwurf-resume-args.ts +25 -63
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-session-identity.ts +7 -6
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +9 -7
- package/scripts/check-entwurf-v2-spawn-production.ts +8 -4
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-fresh-cut-gate.sh +305 -4
- package/scripts/check-gate-qualification.ts +785 -0
- package/scripts/check-meta-identity-consumers.ts +501 -1
- package/scripts/check-meta-listing.ts +91 -9
- package/scripts/check-meta-receiver-marker.ts +54 -0
- package/scripts/check-model-lock.ts +1 -1
- package/scripts/check-shell-quote.ts +2 -1
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/meta-bridge-fresh-cut.ts +164 -28
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/pi_settings_io.py +65 -0
- package/scripts/register-pi-package.py +183 -37
- package/scripts/register-pi-provider.py +68 -10
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/scripts/smoke-meta-install-state.sh +1 -1
- package/scripts/smoke-pi-attach.ts +7 -2
- package/scripts/smoke-user-scope-citizen.sh +177 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- package/scripts/check-entwurf-mailbox-guard.ts +0 -262
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
* Send-is-throw cleanup; see note above.)
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
|
-
import { existsSync, promises as fs
|
|
64
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
65
65
|
import * as net from "node:net";
|
|
66
66
|
import * as os from "node:os";
|
|
67
67
|
import * as path from "node:path";
|
|
@@ -108,9 +108,10 @@ const SESSION_MESSAGE_TYPE = "entwurf-message";
|
|
|
108
108
|
// with the receive-side [entwurf received ⟵] box. The provider-level context
|
|
109
109
|
// filter in index.ts drops this customType before the LLM sees it — colocated
|
|
110
110
|
// with the emitter so sessions without --entwurf-control are still protected.
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
111
|
+
// There is no second emitter: the v1 native send path ("Layer A") that also drew
|
|
112
|
+
// this box through renderSentMessage() was removed in the 0.12 cutover, and
|
|
113
|
+
// renderSentMessage is now registered for exactly one customType (see
|
|
114
|
+
// registerMessageRenderer below). Do not describe a native sender-side box.
|
|
114
115
|
const SENDER_INFO_PATTERN = /<sender_info>[\s\S]*?<\/sender_info>/g;
|
|
115
116
|
|
|
116
117
|
// ============================================================================
|
|
@@ -509,8 +510,8 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
509
510
|
};
|
|
510
511
|
|
|
511
512
|
// Sender-side payload — what renderSentMessage needs to draw the [entwurf sent →]
|
|
512
|
-
// box. Carried
|
|
513
|
-
//
|
|
513
|
+
// box. Carried by the ACP path's CustomMessage `details` — the ONLY carrier since
|
|
514
|
+
// the v1 native renderResult path was removed (0.12 cutover). All four envelope fields are intentionally
|
|
514
515
|
// echoed in the box even though the sender is "this same session" — operators
|
|
515
516
|
// reading a busy multi-session transcript should be able to verify at a glance
|
|
516
517
|
// which 담당자 is on the wire (cwd) and which model identity (agentId) actually
|
|
@@ -520,12 +521,13 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
520
521
|
// re-renders (resize, expand toggle) keep showing the moment the message was
|
|
521
522
|
// actually delivered rather than drifting forward to "now".
|
|
522
523
|
//
|
|
523
|
-
// wants_reply mirrors the receive-side etiquette badge.
|
|
524
|
-
//
|
|
525
|
-
//
|
|
524
|
+
// wants_reply mirrors the receive-side etiquette badge. It stays optional because
|
|
525
|
+
// the box is drawn from a CustomMessage whose `details` may predate the field; the
|
|
526
|
+
// v1 `registerSessionTool` / `entwurfSendParameters` schema this note used to point
|
|
527
|
+
// at is gone (0.12 cutover), so there is no native call site left to grow.
|
|
526
528
|
interface SentBoxData {
|
|
527
529
|
to: string; // target sessionId
|
|
528
|
-
from?: string; // sender agentId, e.g. "entwurf/claude-opus-
|
|
530
|
+
from?: string; // sender agentId, e.g. "entwurf/claude-opus-5"
|
|
529
531
|
cwd?: string; // sender cwd (raw, abbreviateHome applied at render)
|
|
530
532
|
timestamp?: string; // ISO 8601 UTC; rendered in KST
|
|
531
533
|
mode?: string; // "steer" | "follow_up" | string passed through
|
|
@@ -1274,9 +1276,18 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1274
1276
|
target: Type.String({ description: "Target garden id (use entwurf_peers to discover)" }),
|
|
1275
1277
|
intent: StringEnum(["fire-and-forget", "owned-outcome"] as const, {
|
|
1276
1278
|
description:
|
|
1277
|
-
"fire-and-forget = send/reply/hand-off to a LIVE
|
|
1278
|
-
"
|
|
1279
|
-
"(
|
|
1279
|
+
"fire-and-forget = send/reply/hand-off to a LIVE socket target (currently backend pi) or to any " +
|
|
1280
|
+
"citizen with no socket liveness — the decider picks that citizen's rail, and a rail can also " +
|
|
1281
|
+
"REJECT: self-fetch (e.g. Claude Code) → meta-bridge mailbox while that mailbox is DELIVERABLE, " +
|
|
1282
|
+
"else rejected as mailbox-undeliverable; native-push (e.g. Antigravity) → direct injection into " +
|
|
1283
|
+
"its live conversation, which has NO mailbox, and its probe is three-valued — alive: injected, " +
|
|
1284
|
+
"dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate (two rejects, " +
|
|
1285
|
+
"not one). " +
|
|
1286
|
+
"Set wants_reply for an answer. " +
|
|
1287
|
+
"owned-outcome = wake a DORMANT socket-domain citizen via spawn-bg resume ONLY — on a live target " +
|
|
1288
|
+
"rejected as owned-live-no-autosend, on self-fetch as backend-liveness-unsupported, on " +
|
|
1289
|
+
"native-push as native-push-no-resume-authority (both lack resume authority, but the reasons " +
|
|
1290
|
+
"differ because native-push IS probe-measured), and never auto-converted",
|
|
1280
1291
|
}),
|
|
1281
1292
|
message: Type.String({
|
|
1282
1293
|
description:
|
|
@@ -1285,7 +1296,9 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1285
1296
|
}),
|
|
1286
1297
|
mode: Type.Optional(
|
|
1287
1298
|
StringEnum(["steer", "follow_up"] as const, {
|
|
1288
|
-
description:
|
|
1299
|
+
description:
|
|
1300
|
+
"Injection style for a CONTROL-SOCKET send only: steer (immediate) or follow_up (after task). " +
|
|
1301
|
+
"The mailbox, native-push, and spawn-bg plans carry no mode, so it has no effect on those rails.",
|
|
1289
1302
|
}),
|
|
1290
1303
|
),
|
|
1291
1304
|
wants_reply: Type.Optional(Type.Boolean({ description: "Human-conversation reply hint (default false)" })),
|
|
@@ -1299,6 +1312,11 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1299
1312
|
wants_reply?: boolean;
|
|
1300
1313
|
};
|
|
1301
1314
|
|
|
1315
|
+
// TS2589 ("type instantiation is excessively deep") workaround: pi's registerTool
|
|
1316
|
+
// generic infers the handler signature from the TypeBox schema, and this schema is
|
|
1317
|
+
// deep enough to blow the instantiation budget. Casting the FUNCTION (not the
|
|
1318
|
+
// argument) keeps the schema itself typed while stopping the inference walk.
|
|
1319
|
+
// Revisit when pi's registerTool takes an explicit params type parameter.
|
|
1302
1320
|
const registerTool = pi.registerTool as (def: any) => void;
|
|
1303
1321
|
|
|
1304
1322
|
registerTool({
|
|
@@ -1306,28 +1324,51 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
|
|
|
1306
1324
|
label: "Dispatch (v2)",
|
|
1307
1325
|
description: `CANONICAL delivery surface for a garden id. When you have a garden id and want to
|
|
1308
1326
|
reach whoever it names — message / reply / hand-off — use THIS verb. A garden id alone does not
|
|
1309
|
-
reveal
|
|
1327
|
+
reveal which rail that citizen answers on — a live socket session, a dormant one, a mailbox-backed
|
|
1328
|
+
self-fetch session, or a native-push session —
|
|
1310
1329
|
and entwurf_v2 is the one surface that reads that and routes correctly (so "when unsure which
|
|
1311
1330
|
transport, use entwurf_v2"). It dispatches to EXISTING targets; brand-new sibling creation is deferred
|
|
1312
1331
|
to a later v2 lane. Dispatch to a garden citizen through the unified entwurf_v2 verb: the 5b decider
|
|
1313
|
-
picks the transport (live control-socket send / spawn-bg resume / meta-mailbox enqueue
|
|
1314
|
-
target's liveness + your intent, runs it under the v2 lock policy
|
|
1315
|
-
|
|
1316
|
-
|
|
1332
|
+
picks the transport (live control-socket send / spawn-bg resume / meta-mailbox enqueue / native-push
|
|
1333
|
+
direct injection) from the target's liveness + your intent, runs it under the v2 lock policy, and reports
|
|
1334
|
+
one outcome (delivered / rejected / lock-retained / delivered-but-lock-dirty).
|
|
1335
|
+
|
|
1336
|
+
LOCK POLICY (do not over-generalize it): the per-target lock is taken for a control-socket-DOMAIN
|
|
1337
|
+
dispatch, which is BOTH the live send AND the dormant cell's spawn-bg resume — spawn-bg is a separate
|
|
1338
|
+
relaunch transport yet it still runs under that domain's lock. The mailbox and native-push rails are
|
|
1339
|
+
lock-free: the mailbox is guarded instead by active-receiver deliverability, and native-push by its
|
|
1340
|
+
adapter probe.
|
|
1317
1341
|
|
|
1318
1342
|
- target: the garden id of the citizen to reach (required).
|
|
1319
1343
|
- intent: fire-and-forget (a send with no owned result) or owned-outcome (you own the result).
|
|
1320
1344
|
- message: the message/prompt to dispatch (required).
|
|
1321
|
-
- mode: steer or follow_up for a
|
|
1322
|
-
-
|
|
1345
|
+
- mode: steer or follow_up for a CONTROL-SOCKET send (optional). The mailbox, native-push, and
|
|
1346
|
+
spawn-bg plans carry no mode, so it has no effect on those rails — a native-push send IS live and
|
|
1347
|
+
still ignores it.
|
|
1348
|
+
- wants_reply: reply hint; it rides every rail (optional, default false).
|
|
1323
1349
|
|
|
1324
1350
|
CHOOSING INTENT (picking wrong is rejected, never auto-fixed): to message / reply / hand off a peer
|
|
1325
|
-
that entwurf_peers shows as liveness=alive (a live
|
|
1326
|
-
— it routes to the live control-socket; set wants_reply:true if you need an answer
|
|
1327
|
-
owned-outcome).
|
|
1328
|
-
fire-and-forget
|
|
1329
|
-
|
|
1330
|
-
|
|
1351
|
+
that entwurf_peers shows as liveness=alive (a live socket citizen, currently backend pi), use intent:
|
|
1352
|
+
fire-and-forget — it routes to the live control-socket; set wants_reply:true if you need an answer
|
|
1353
|
+
(wants_reply is NOT owned-outcome). Replies to a citizen with NO socket liveness
|
|
1354
|
+
(liveness=unsupported) are ALSO fire-and-forget, and the decider picks that citizen's own rail: a
|
|
1355
|
+
self-fetch backend (e.g. Claude Code) gets the meta-bridge mailbox, while a native-push backend
|
|
1356
|
+
(e.g. Antigravity) gets direct injection into its live conversation and has NO mailbox at all — do
|
|
1357
|
+
not assume mailbox semantics for every unsupported citizen. A native-push target IS measured by its
|
|
1358
|
+
own adapter probe, and that probe is THREE-valued, so the send is never silently queued: alive →
|
|
1359
|
+
injected; dead → rejected as native-push-target-dead; indeterminate → rejected as
|
|
1360
|
+
native-push-probe-indeterminate. Those last two stay separate on purpose — "we could not establish
|
|
1361
|
+
it" is not "it is gone". THERE IS A THIRD RESULT, so do not read
|
|
1362
|
+
liveness=unsupported as "reachable by some rail": the mailbox path delivers only to a DELIVERABLE
|
|
1363
|
+
citizen (a self-fetch backend whose receiver is live and armed), so a terminated Claude Code session —
|
|
1364
|
+
and any record whose backend has no adapter on this lane, e.g. codex — is rejected as
|
|
1365
|
+
mailbox-undeliverable rather than queued into an inbox nobody drains. unsupported means only "this
|
|
1366
|
+
backend has no control-socket probe".
|
|
1367
|
+
owned-outcome is ONLY for waking a DORMANT socket-domain citizen (spawn-bg resume, currently backend
|
|
1368
|
+
pi); on a live target it is rejected as owned-live-no-autosend. Neither self-fetch nor native-push
|
|
1369
|
+
has resume authority, but they reject under DIFFERENT reasons — self-fetch as
|
|
1370
|
+
backend-liveness-unsupported, native-push as native-push-no-resume-authority.
|
|
1371
|
+
It is NEVER auto-converted — so pick the right intent up front.
|
|
1331
1372
|
|
|
1332
1373
|
The decider — not this surface — chooses the transport.`,
|
|
1333
1374
|
parameters: entwurfV2Parameters,
|
|
@@ -1396,7 +1437,7 @@ The decider — not this surface — chooses the transport.`,
|
|
|
1396
1437
|
|
|
1397
1438
|
interface EntwurfFactProviderModule {
|
|
1398
1439
|
listEntwurfFacts(params: {
|
|
1399
|
-
metaEntries: readonly string[];
|
|
1440
|
+
metaEntries: readonly { filename: string; regularFile: boolean }[];
|
|
1400
1441
|
readRecord: (filename: string) => string;
|
|
1401
1442
|
socket: { dir: string };
|
|
1402
1443
|
}): Promise<unknown>;
|
|
@@ -1408,22 +1449,22 @@ interface EntwurfPeersRenderModule {
|
|
|
1408
1449
|
|
|
1409
1450
|
interface MetaSessionModule {
|
|
1410
1451
|
defaultMetaSessionsDir(): string;
|
|
1452
|
+
readActiveStoreEntries(dir: string): { filename: string; regularFile: boolean }[];
|
|
1453
|
+
makeStoreRecordReader(dir: string): (filename: string) => string;
|
|
1411
1454
|
}
|
|
1412
1455
|
|
|
1413
1456
|
async function renderEntwurfPeersForSurface(): Promise<{ text: string; payload: unknown }> {
|
|
1414
1457
|
const meta = (await import(META_SESSION_MODULE)) as unknown as MetaSessionModule;
|
|
1415
1458
|
const sessionsDir = meta.defaultMetaSessionsDir();
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1459
|
+
// Entries carry their KIND. The name-only readdir this used to do left `readRecord`
|
|
1460
|
+
// free to follow a symlinked `.meta.json` into bytes the store does not own — rule 1
|
|
1461
|
+
// held in the doctor and not on the surface operators actually read (pre-existing;
|
|
1462
|
+
// surfaced by the #52 duplicate pass, which would let such a symlink quarantine the
|
|
1463
|
+
// healthy record it shadowed).
|
|
1423
1464
|
const provider = (await import(ENTWURF_FACT_PROVIDER_MODULE)) as unknown as EntwurfFactProviderModule;
|
|
1424
1465
|
const result = await provider.listEntwurfFacts({
|
|
1425
|
-
metaEntries,
|
|
1426
|
-
readRecord:
|
|
1466
|
+
metaEntries: meta.readActiveStoreEntries(sessionsDir),
|
|
1467
|
+
readRecord: meta.makeStoreRecordReader(sessionsDir),
|
|
1427
1468
|
// Same socket axis as the legacy live-session scan, but merged with the
|
|
1428
1469
|
// meta-record rail by listEntwurfFacts so meta-mailbox citizens are discoverable too.
|
|
1429
1470
|
socket: { dir: ENTWURF_DIR },
|
|
@@ -1433,8 +1474,9 @@ async function renderEntwurfPeersForSurface(): Promise<{ text: string; payload:
|
|
|
1433
1474
|
}
|
|
1434
1475
|
|
|
1435
1476
|
function registerListSessionsTool(pi: ExtensionAPI): void {
|
|
1436
|
-
// Same TS2589 workaround as
|
|
1437
|
-
//
|
|
1477
|
+
// Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there
|
|
1478
|
+
// for the revisit conditions. (It used to point at registerSessionTool, which was
|
|
1479
|
+
// removed in the 0.12 cutover, so the pointer dangled.)
|
|
1438
1480
|
const registerTool = pi.registerTool as (def: any) => void;
|
|
1439
1481
|
registerTool({
|
|
1440
1482
|
name: "entwurf_peers",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
// Why `/compat` and NOT the 0.80 provider-factory subpath (the pi-ai
|
|
19
19
|
// providers/anthropic subpath): this file is loaded by pi's
|
|
20
20
|
// EXTENSION loader (pi-coding-agent `core/extensions/loader.ts`), whose jiti
|
|
21
|
-
// alias map resolves
|
|
22
|
-
// `/compat`,
|
|
23
|
-
//
|
|
24
|
-
// and appends the remainder, yielding the
|
|
25
|
-
// `…/dist/compat.js/providers/anthropic` (verified live: extension
|
|
26
|
-
// invisible to static typecheck which resolves against node_modules
|
|
27
|
-
// So `/compat`
|
|
28
|
-
// model-catalog API, and the SINGLE allowlisted exception in
|
|
21
|
+
// alias map resolves FOUR pi-ai specifiers for extensions — the bare root,
|
|
22
|
+
// `/compat`, `/oauth`, and (since pi 0.81) `/providers/all`. Other `providers/*`
|
|
23
|
+
// subpaths are NOT in that map: jiti prefix-matches the bare
|
|
24
|
+
// `@earendil-works/pi-ai` alias and appends the remainder, yielding the
|
|
25
|
+
// unresolvable `…/dist/compat.js/providers/anthropic` (verified live: extension
|
|
26
|
+
// load crash, invisible to static typecheck which resolves against node_modules
|
|
27
|
+
// `exports`). So `/compat` remains the SINGLE sanctioned extension entrypoint
|
|
28
|
+
// this repo uses for the old global model-catalog API, and the SINGLE allowlisted exception in
|
|
29
29
|
// `run.sh check-pi-import-surface`. `getModels` here is compat's deprecated
|
|
30
30
|
// re-export of `getBuiltinModels`. When pi removes compat we migrate to whatever
|
|
31
31
|
// the loader then exposes.
|
|
@@ -50,13 +50,13 @@ export const ENTWURF_ACP_NO_AUTH_SENTINEL = "entwurf-no-auth";
|
|
|
50
50
|
// both axes (protocol smoke + agent interview) — do not extend casually.
|
|
51
51
|
// Exported so the claude backend adapter (backend-adapter.ts) can answer
|
|
52
52
|
// `routeModel` without re-deriving the set from curatedClaudeModels().
|
|
53
|
-
export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-5", "claude-opus-
|
|
53
|
+
export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-5", "claude-opus-5"] as const;
|
|
54
54
|
|
|
55
55
|
/** The anchor model whose absence is a hard registry regression, not a soft skip. */
|
|
56
|
-
export const CURATED_ANCHOR_MODEL_ID = "claude-opus-
|
|
56
|
+
export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5";
|
|
57
57
|
|
|
58
|
-
// Anthropic's registry reports 1M for both Sonnet 5 and Opus
|
|
59
|
-
// entwurf surface
|
|
58
|
+
// Anthropic's registry reports 1M for both Sonnet 5 and Opus 5, and the
|
|
59
|
+
// entwurf surface exposes the full 1M for BOTH. Sonnet 5's 1M window is the
|
|
60
60
|
// whole point of the 0.12.3 bump — it is the compact-free long-context floor the
|
|
61
61
|
// earlier 200K Sonnet cap could not provide. We still clamp to a 1M ceiling so a
|
|
62
62
|
// future registry value can't silently inflate the surface past what we verify.
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
// operator entries a backend needs (credentials, caches, built-in skills)
|
|
10
10
|
// reachable through a TIGHT symlink whitelist — nothing else.
|
|
11
11
|
//
|
|
12
|
-
// Scope (NEXT §스코프 / §S2-scout 핀3): Claude-only on this lane.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// Scope (NEXT §스코프 / §S2-scout 핀3): Claude-only on this lane. Claude is the one
|
|
13
|
+
// shipped ACP backend, so this materializer is written for its config surface alone.
|
|
14
|
+
// Codex/Gemini overlays (CODEX_HOME / admin.toml) are behavior-oracle territory and
|
|
15
|
+
// out of scope here; a SECOND ACP backend enters only once the entry conditions in
|
|
16
|
+
// docs/acp-backend-rail.md are met, and it brings its own overlay.
|
|
15
17
|
//
|
|
16
18
|
// Two deliberate divergences from the literal 0.11.0 illustrative comment block
|
|
17
19
|
// (the 0.11.0 CODE already does both — only its top doc-comment drew projects/
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* entwurf-core — sync entwurf execution, host-agnostic.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - pi-extensions/entwurf.ts (pi
|
|
6
|
-
* -
|
|
4
|
+
* DIRECT importers, MEASURED from the import graph (2026-07-27) — not from memory:
|
|
5
|
+
* - pi-extensions/entwurf-control.ts (the pi adapter)
|
|
6
|
+
* - pi-extensions/lib/entwurf-v2-spawn-production.ts (v2 spawn-bg launch identity)
|
|
7
|
+
* - pi-extensions/lib/project-trust-handler.ts · lib/acp/augment.ts ·
|
|
8
|
+
* lib/native-push/adapter.ts
|
|
9
|
+
* - plus gates/smokes (check-shell-quote, check-package-source-routing, …)
|
|
10
|
+
*
|
|
11
|
+
* The MCP bridge is a TRANSITIVE consumer, not a direct one, and the distinction is the
|
|
12
|
+
* whole point: `mcp/entwurf-bridge/src/index.ts → entwurf-v2-surface.ts →
|
|
13
|
+
* entwurf-v2-production.ts → entwurf-v2-spawn-production.ts → entwurf-core.ts`. So the old
|
|
14
|
+
* header's claim — that the bridge imports this shared implementation the way a v1 tool
|
|
15
|
+
* surface did — was false, but so is "the bridge does not reach it": it reaches it at
|
|
16
|
+
* runtime, only for spawn-bg production.
|
|
17
|
+
*
|
|
18
|
+
* NOT consumers on any path: `pi-extensions/entwurf.ts` (the v1 pi native tool surface) and
|
|
19
|
+
* `pi-extensions/lib/entwurf-async.ts` were REMOVED in the 0.12 cutover. Do not reintroduce
|
|
20
|
+
* either as a live consumer.
|
|
7
21
|
*
|
|
8
22
|
* This module MUST NOT import anything from @earendil-works/pi-coding-agent or any
|
|
9
|
-
* other pi runtime API. It is pure Node + @sinclair/typebox-free.
|
|
10
|
-
* requires pi's ExtensionAPI (sendMessage, appendEntry, sessionManager) belongs
|
|
11
|
-
*
|
|
23
|
+
* other pi runtime API. It is pure Node + @sinclair/typebox-free. Anything that
|
|
24
|
+
* requires pi's ExtensionAPI (sendMessage, appendEntry, sessionManager) belongs to a
|
|
25
|
+
* pi-adapter surface, never here — the v1 async entwurf path that used to hold that
|
|
26
|
+
* work went away with `pi-extensions/entwurf.ts`.
|
|
12
27
|
*
|
|
13
28
|
* Scope:
|
|
14
29
|
* - sync execution (spawn pi, collect message_end events, return summary)
|
|
@@ -67,9 +82,11 @@ const PI_SETTINGS_PATH = process.env.PI_SETTINGS_PATH
|
|
|
67
82
|
: path.join(AGENT_DIR, "settings.json");
|
|
68
83
|
export const ENTWURF_CODEX_ACP_ENV = "ENTWURF_ACP_FOR_CODEX";
|
|
69
84
|
|
|
70
|
-
// Currently unused: remote/SSH entwurf is fail-fast
|
|
71
|
-
//
|
|
72
|
-
// scripts/check-shell-quote.ts
|
|
85
|
+
// Currently unused: remote/SSH entwurf is fail-fast (garden-native identity is
|
|
86
|
+
// local-FS only). Retained for #11 remote revival and pinned by
|
|
87
|
+
// scripts/check-shell-quote.ts, whose SOURCE_SITES is now THIS FILE ALONE — the
|
|
88
|
+
// entwurf.ts / entwurf-async.ts halves of that former parity check were removed
|
|
89
|
+
// with those files, so there is no cross-file parity left to assert.
|
|
73
90
|
// biome-ignore lint/correctness/noUnusedVariables: retained for #11 remote revival; parity-gated.
|
|
74
91
|
function shellQuote(value: string): string {
|
|
75
92
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* drains its own inbox on wake. A DIRECT-INJECT backend (pi / codex / antigravity)
|
|
19
19
|
* has no mailbox drain at all — enqueuing for it is the SE-1 false success
|
|
20
20
|
* ("✓ delivered" into a void). So deliverable = wakeMode === "self-fetch" AND the
|
|
21
|
-
* receiver is active.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* receiver is active. Every enqueue site must pass this guard before writing a .msg
|
|
22
|
+
* (slice 2d). The shipped sites are the v2 decider and its send-fallback re-resolve;
|
|
23
|
+
* the v1 fallback / MCP v1 / pi-native v1 sites this once also listed were removed in
|
|
24
|
+
* the 0.12 cutover — do not read them as live.
|
|
24
25
|
*
|
|
25
26
|
* - nativePushDeliverable(facts): the SEPARATE deliverability predicate for a
|
|
26
27
|
* NATIVE-PUSH backend (antigravity). A native-push citizen has no mailbox and no
|
|
@@ -89,9 +90,12 @@ export interface ReceiverIdentityFacts {
|
|
|
89
90
|
* Does this presence marker actually belong to the target identity? A marker that is
|
|
90
91
|
* absent, or whose garden id / backend / native session id has drifted from the record,
|
|
91
92
|
* is NOT this receiver — fail-closed (a stale/foreign marker must never raise a dead
|
|
92
|
-
* target to "active"). The single source of truth for "marker ↔ identity match"
|
|
93
|
-
*
|
|
94
|
-
* `
|
|
93
|
+
* target to "active"). The single source of truth for "marker ↔ identity match". Its
|
|
94
|
+
* PRODUCTION consumers are the v2 `mailboxDeliverabilityFor` seam and the MCP bridge's
|
|
95
|
+
* `entwurf_self`. There is no second implementation: `entwurf-mailbox-guard.ts` used to
|
|
96
|
+
* wrap this atom with its own enqueue orchestration, had ZERO production importers (import
|
|
97
|
+
* graph measured 2026-07-27), and was DELETED rather than left as a green gate proving only
|
|
98
|
+
* retired behaviour. A new enqueue site consults this predicate through that seam.
|
|
95
99
|
*/
|
|
96
100
|
export function receiverMarkerMatchesIdentity(
|
|
97
101
|
marker: ReceiverIdentityFacts | null | undefined,
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* (one-way: provider → facts / socket-discovery / meta-session) — no import cycle
|
|
6
6
|
* with `entwurf-facts.ts` (which owns `SocketProbe`/`resolveFactList`).
|
|
7
7
|
*
|
|
8
|
-
* listAllMetaIdentities →
|
|
9
|
-
* → pre-quarantine
|
|
8
|
+
* listAllMetaIdentities → socket-domain gid extraction → scanSocketProbes
|
|
9
|
+
* → pre-quarantine out-of-domain/socket conflicts → resolveFactList(clean)
|
|
10
10
|
* → { facts, diagnostics }
|
|
11
11
|
*
|
|
12
12
|
* Two throw-vs-diagnostics policies, kept distinct (GPT힣 C-원칙):
|
|
@@ -20,16 +20,21 @@
|
|
|
20
20
|
* — that is not a re-implementation of the collision rule, it is input
|
|
21
21
|
* sanitation that leaves the pure-core invariant intact.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* socket): gardenId is the universal address and a send path
|
|
23
|
+
* An out-of-socket-domain record/socket collision quarantines BOTH sides (the
|
|
24
|
+
* PeerFact AND the socket): gardenId is the universal address and a send path
|
|
25
25
|
* first, so surfacing the record alone (as a clean `unsupported` PeerFact) while
|
|
26
26
|
* a same-gid socket exists would be half a lie. Both leave the normal output;
|
|
27
27
|
* one diagnostic carries the fact. (pi + same-gid socket = the normal merge.)
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { type FactList,
|
|
30
|
+
import { type FactList, isOutOfSocketDomainGardenIdConflict, resolveFactList } from "./entwurf-facts.ts";
|
|
31
31
|
import { isLivenessSupported } from "./entwurf-v2-contract.ts";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
type ActiveStoreEntry,
|
|
34
|
+
FRESH_CUT_PRESCRIPTION,
|
|
35
|
+
listAllMetaIdentities,
|
|
36
|
+
type MetaCitizenBackend,
|
|
37
|
+
} from "./meta-session.ts";
|
|
33
38
|
import { type SocketScanDeps, scanSocketProbes } from "./socket-discovery.ts";
|
|
34
39
|
import type { SocketLiveness } from "./socket-probe.ts";
|
|
35
40
|
|
|
@@ -82,7 +87,9 @@ export interface EntwurfFactsResult {
|
|
|
82
87
|
|
|
83
88
|
export interface EntwurfFactsDeps {
|
|
84
89
|
/** Meta-store axis: the `.meta.json` entry names + a record reader. */
|
|
85
|
-
|
|
90
|
+
/** Store entries WITH their kind (`readActiveStoreEntries`), never bare names: the
|
|
91
|
+
* listing must be able to refuse a symlinked record without following it. */
|
|
92
|
+
metaEntries: readonly ActiveStoreEntry[];
|
|
86
93
|
readRecord: (filename: string) => string;
|
|
87
94
|
/** Socket axis: injected into scanSocketProbes (controlDir/readdir/probe). */
|
|
88
95
|
socket?: Partial<SocketScanDeps>;
|
|
@@ -123,8 +130,8 @@ export async function listEntwurfFacts(deps: EntwurfFactsDeps): Promise<EntwurfF
|
|
|
123
130
|
// 2. socket axis — probe (dir sockets) ∪ (in-domain citizen canonical paths).
|
|
124
131
|
// Its three hazards (symlink forgery / malformed name / dir-read error) are
|
|
125
132
|
// folded into diagnostics here so the listing survives but never lies.
|
|
126
|
-
const
|
|
127
|
-
const scan = await scanSocketProbes(
|
|
133
|
+
const socketDomainGids = identities.filter((i) => isLivenessSupported(i.backend)).map((i) => i.gardenId);
|
|
134
|
+
const scan = await scanSocketProbes(socketDomainGids, deps.socket ?? {});
|
|
128
135
|
const probes = scan.probes;
|
|
129
136
|
const socketGids = new Set(probes.map((p) => p.gardenId));
|
|
130
137
|
const symlinkedGids = new Set(scan.symlinkedGardenIds);
|
|
@@ -151,24 +158,22 @@ export async function listEntwurfFacts(deps: EntwurfFactsDeps): Promise<EntwurfF
|
|
|
151
158
|
});
|
|
152
159
|
}
|
|
153
160
|
|
|
154
|
-
// 3.
|
|
155
|
-
// predicate is SHARED with
|
|
156
|
-
// listing and dispatch cannot drift
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// (symlinked) socket survive as a clean PeerFact while the legacy send path
|
|
160
|
-
// still followed the symlink — the gap this closes.
|
|
161
|
+
// 3. Pre-quarantine citizens outside the control-socket capability domain that
|
|
162
|
+
// collide with a control socket. The predicate is SHARED with v2 dispatch so
|
|
163
|
+
// listing and dispatch cannot drift. It unions real and symlinked socket gids:
|
|
164
|
+
// the earlier real-socket-only check let an out-of-domain record survive as a
|
|
165
|
+
// clean PeerFact beside a forged symlink receiver.
|
|
161
166
|
const conflictGids = new Set<string>();
|
|
162
167
|
for (const id of identities) {
|
|
163
|
-
if (
|
|
168
|
+
if (isOutOfSocketDomainGardenIdConflict(id.backend, id.gardenId, socketGids, symlinkedGids)) {
|
|
164
169
|
conflictGids.add(id.gardenId);
|
|
165
170
|
diagnostics.push({
|
|
166
171
|
kind: "garden-id-socket-conflict",
|
|
167
172
|
gardenId: id.gardenId,
|
|
168
173
|
backend: id.backend,
|
|
169
174
|
message:
|
|
170
|
-
`
|
|
171
|
-
"
|
|
175
|
+
`out-of-socket-domain citizen (${id.backend}) shares its gardenId with a control socket ` +
|
|
176
|
+
"(real or symlinked) — address ambiguity; both sides are quarantined from the listing.",
|
|
172
177
|
});
|
|
173
178
|
}
|
|
174
179
|
}
|
|
@@ -75,27 +75,27 @@ export function resolvePeerFact(identity: MetaIdentity, socket: SocketLiveness |
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
* one OR a symlinked/forged one. The gardenId is the universal
|
|
80
|
-
* so a
|
|
81
|
-
* socket
|
|
78
|
+
* An OUT-OF-SOCKET-DOMAIN record whose gardenId collides with a control socket —
|
|
79
|
+
* a real (probed) one OR a symlinked/forged one. The gardenId is the universal
|
|
80
|
+
* address, so a citizen without socket capability sharing it with a socket means
|
|
81
|
+
* the socket reaches a DIFFERENT receiver than the record names — an address split.
|
|
82
82
|
* Both the citizen and the socket are quarantined from the facts listing.
|
|
83
83
|
*
|
|
84
84
|
* The union `socketGids ∪ symlinkedGardenIds` is load-bearing: `socketGids` are
|
|
85
85
|
* gids with a real probed `*.sock`, but `symlinkedGardenIds` are NEVER probed (P1)
|
|
86
86
|
* and so are absent from `socketGids`. Looking at `socketGids` alone (the
|
|
87
|
-
* fact-provider:125 gap this closes) let
|
|
87
|
+
* fact-provider:125 gap this closes) let an out-of-domain citizen with a *symlinked* socket
|
|
88
88
|
* survive as a clean PeerFact while the legacy send path still followed the symlink
|
|
89
89
|
* to a forged receiver. Both axes claim the gid → both must quarantine it.
|
|
90
90
|
*
|
|
91
|
-
* SCOPE: this is the
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
91
|
+
* SCOPE: this is the record-side, out-of-socket-domain conflict only — shared by
|
|
92
|
+
* listing and dispatch so they cannot drift; only the observation-bit source is
|
|
93
|
+
* parameterized. A citizen IN the socket domain whose own canonical socket is a
|
|
94
|
+
* symlink is NOT this predicate's concern — that is a
|
|
95
95
|
* target-specific lstat conflict the decider's `inspectTargetControlSocket` raises
|
|
96
96
|
* as `address-conflict`, kept deliberately separate (GPT 1차 검수 C).
|
|
97
97
|
*/
|
|
98
|
-
export function
|
|
98
|
+
export function isOutOfSocketDomainGardenIdConflict(
|
|
99
99
|
backend: string,
|
|
100
100
|
gardenId: string,
|
|
101
101
|
socketGids: ReadonlySet<string>,
|
|
@@ -163,8 +163,8 @@ export interface FactList {
|
|
|
163
163
|
* `indeterminate` and strand a dormant citizen as un-resumable); a dormant
|
|
164
164
|
* citizen's absent socket file is probed to `dead` (ENOENT) by the wiring and
|
|
165
165
|
* arrives here AS `dead` → dormant → resumable.
|
|
166
|
-
* - out-of-domain citizen WITH a control socket at its gardenId → fail-loud
|
|
167
|
-
* (address ambiguity; a
|
|
166
|
+
* - out-of-socket-domain citizen WITH a control socket at its gardenId → fail-loud
|
|
167
|
+
* (address ambiguity; a backend without socket capability cannot own that rail).
|
|
168
168
|
* - out-of-domain citizen without a socket → `unsupported` (via resolvePeerFact).
|
|
169
169
|
* - a probed gardenId with NO citizen → `RecordLessSocketFact` (#50 C4: a
|
|
170
170
|
* diagnostic subject the provider folds into a `record-less-socket`
|
|
@@ -202,7 +202,7 @@ export function resolveFactList(identities: MetaIdentity[], socketProbes: Socket
|
|
|
202
202
|
if (probeMap.has(gid)) {
|
|
203
203
|
throw new Error(
|
|
204
204
|
`resolveFactList: out-of-domain citizen ${gid} (${identity.backend}) has a control socket — ` +
|
|
205
|
-
"address ambiguity (
|
|
205
|
+
"address ambiguity (an out-of-socket-domain citizen cannot own a control socket)",
|
|
206
206
|
);
|
|
207
207
|
}
|
|
208
208
|
socket = null;
|