instar 1.3.593 → 1.3.595

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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * DemoChannelRegistry — the §5.3 isolation backbone of the live-user-channel-proof
3
+ * standard (docs/specs/live-user-channel-proof-standard.md §5.3 "Demo Channel
4
+ * Isolation").
5
+ *
6
+ * A volatile / permission / dangerous live-test scenario must NEVER touch the live
7
+ * operator channel — it runs only on a registered DEMO channel (a throwaway Slack
8
+ * workspace, a demo Telegram group). The LiveTestHarness enforces this structurally
9
+ * by calling `isDemoChannel(surface, channelId)` and refusing the whole run (a throw,
10
+ * not a convention) if a non-safe scenario points at a channel this registry does not
11
+ * vouch for.
12
+ *
13
+ * The bindings are SIGNED and read once. A binding set that fails signature
14
+ * verification (tampered, unsigned, wrong signer) is FAIL-CLOSED: every channel
15
+ * resolves to NOT-a-demo-channel, so the harness's §5.3 guard refuses every volatile
16
+ * scenario rather than trusting an unverified "this is a safe demo channel" claim.
17
+ * Fail-closed is the safe direction here — the cost of a false "not demo" is a refused
18
+ * test; the cost of a false "is demo" is a destructive scenario on the live channel.
19
+ *
20
+ * This module is pure/injectable: it takes the raw bindings document + a `verify`
21
+ * function (Ed25519, same signer surface the LiveTestArtifactStore uses) so it is
22
+ * unit-testable with no filesystem or crypto host.
23
+ */
24
+ import type { Surface } from './LiveTestArtifactStore.js';
25
+ /** One registered demo channel. `workspaceId` is Slack-only (the demo workspace). */
26
+ export interface DemoChannelBinding {
27
+ surface: Surface;
28
+ /** Slack channel id, Telegram chat/topic id (as a string), or dashboard id. */
29
+ channelId: string;
30
+ /** Slack workspace / team id — REQUIRED for slack, ignored otherwise. */
31
+ workspaceId?: string;
32
+ /** Human label for audit (e.g. "SageMind Live Test"). */
33
+ label?: string;
34
+ }
35
+ /** The on-disk, signed bindings document. */
36
+ export interface DemoChannelBindingsDoc {
37
+ version: 1;
38
+ /** The machine that authored these bindings (audit only). */
39
+ machineId: string;
40
+ bindings: DemoChannelBinding[];
41
+ signedAt: string;
42
+ /** Ed25519 signature over the canonical payload (see canonicalBindingsPayload). */
43
+ signature: string;
44
+ }
45
+ export interface DemoChannelRegistryDeps {
46
+ /** The raw bindings document (e.g. parsed from state/demo-channel-bindings.json), or null when none exist. */
47
+ doc: DemoChannelBindingsDoc | null;
48
+ /** Verify a signature over a canonical payload string. Returns true iff valid. */
49
+ verify: (payload: string, signature: string) => boolean;
50
+ logger?: (m: string) => void;
51
+ }
52
+ /**
53
+ * The exact bytes the signature covers. Stable, order-independent across the binding
54
+ * LIST (sorted) so a re-serialization can't change the signed payload, but the
55
+ * signature is otherwise over the full meaningful content (version + machineId +
56
+ * signedAt + every binding field). Any field change invalidates the signature.
57
+ */
58
+ export declare function canonicalBindingsPayload(doc: Omit<DemoChannelBindingsDoc, 'signature'>): string;
59
+ export declare class DemoChannelRegistry {
60
+ private readonly verified;
61
+ private readonly set;
62
+ private readonly d;
63
+ constructor(deps: DemoChannelRegistryDeps);
64
+ private key;
65
+ private log;
66
+ /** True iff (surface, channelId) is a verified, registered demo channel. */
67
+ isDemoChannel(surface: Surface, channelId: string): boolean;
68
+ /** Whether a present bindings doc verified (false when none exist OR signature failed). */
69
+ get isVerified(): boolean;
70
+ /** Count of registered demo channels (0 when unverified/absent). */
71
+ get size(): number;
72
+ }
73
+ //# sourceMappingURL=DemoChannelRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DemoChannelRegistry.d.ts","sourceRoot":"","sources":["../../src/core/DemoChannelRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6CAA6C;AAC7C,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,CAAC,CAAC;IACX,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,8GAA8G;IAC9G,GAAG,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACnC,kFAAkF;IAClF,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACxD,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,IAAI,CAAC,sBAAsB,EAAE,WAAW,CAAC,GAAG,MAAM,CAkB/F;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA0B;gBAEhC,IAAI,EAAE,uBAAuB;IA6BzC,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,GAAG;IAEX,4EAA4E;IAC5E,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAI3D,2FAA2F;IAC3F,IAAI,UAAU,IAAI,OAAO,CAA0B;IAEnD,oEAAoE;IACpE,IAAI,IAAI,IAAI,MAAM,CAA0B;CAC7C"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * DemoChannelRegistry — the §5.3 isolation backbone of the live-user-channel-proof
3
+ * standard (docs/specs/live-user-channel-proof-standard.md §5.3 "Demo Channel
4
+ * Isolation").
5
+ *
6
+ * A volatile / permission / dangerous live-test scenario must NEVER touch the live
7
+ * operator channel — it runs only on a registered DEMO channel (a throwaway Slack
8
+ * workspace, a demo Telegram group). The LiveTestHarness enforces this structurally
9
+ * by calling `isDemoChannel(surface, channelId)` and refusing the whole run (a throw,
10
+ * not a convention) if a non-safe scenario points at a channel this registry does not
11
+ * vouch for.
12
+ *
13
+ * The bindings are SIGNED and read once. A binding set that fails signature
14
+ * verification (tampered, unsigned, wrong signer) is FAIL-CLOSED: every channel
15
+ * resolves to NOT-a-demo-channel, so the harness's §5.3 guard refuses every volatile
16
+ * scenario rather than trusting an unverified "this is a safe demo channel" claim.
17
+ * Fail-closed is the safe direction here — the cost of a false "not demo" is a refused
18
+ * test; the cost of a false "is demo" is a destructive scenario on the live channel.
19
+ *
20
+ * This module is pure/injectable: it takes the raw bindings document + a `verify`
21
+ * function (Ed25519, same signer surface the LiveTestArtifactStore uses) so it is
22
+ * unit-testable with no filesystem or crypto host.
23
+ */
24
+ /**
25
+ * The exact bytes the signature covers. Stable, order-independent across the binding
26
+ * LIST (sorted) so a re-serialization can't change the signed payload, but the
27
+ * signature is otherwise over the full meaningful content (version + machineId +
28
+ * signedAt + every binding field). Any field change invalidates the signature.
29
+ */
30
+ export function canonicalBindingsPayload(doc) {
31
+ // Each binding is encoded as an ORDERED JSON tuple, NOT a delimiter-free
32
+ // concatenation. A concatenation like `${surface}${channelId}${workspaceId}` is
33
+ // ambiguous at field boundaries ({channelId:'C1',workspaceId:'W2'} vs
34
+ // {channelId:'C1W2'} would serialize identically and share one signature — a
35
+ // real bypass that promotes an unvouched channelId to a demo channel). A JSON
36
+ // tuple makes every field boundary explicit and distinguishes an absent field
37
+ // (null) from a present empty string (''), so one signature covers exactly one
38
+ // binding set.
39
+ const norm = (b) => JSON.stringify([b.surface, b.channelId, b.workspaceId ?? null, b.label ?? null]);
40
+ const lines = [...doc.bindings].map(norm).sort();
41
+ return [
42
+ `v=${doc.version}`,
43
+ `machine=${doc.machineId}`,
44
+ `signedAt=${doc.signedAt}`,
45
+ ...lines.map((l) => `b=${l}`),
46
+ ].join('\n');
47
+ }
48
+ export class DemoChannelRegistry {
49
+ verified;
50
+ set;
51
+ d;
52
+ constructor(deps) {
53
+ this.d = deps;
54
+ const { doc } = deps;
55
+ if (!doc) {
56
+ // No bindings at all is a legitimate state (safe-only test runs). It is NOT an
57
+ // error — it simply means there are zero demo channels, so isDemoChannel is
58
+ // always false and only safe scenarios can run.
59
+ this.verified = false;
60
+ this.set = new Set();
61
+ return;
62
+ }
63
+ const { signature, ...unsigned } = doc;
64
+ let ok = false;
65
+ try {
66
+ ok = this.d.verify(canonicalBindingsPayload(unsigned), signature);
67
+ }
68
+ catch (err) {
69
+ this.log(`verify threw (treating as unverified): ${err instanceof Error ? err.message : String(err)}`);
70
+ ok = false;
71
+ }
72
+ this.verified = ok;
73
+ if (!ok) {
74
+ // Fail-closed: a present-but-unverifiable bindings doc grants ZERO demo channels.
75
+ this.log(`bindings present but signature INVALID — fail-closed (0 demo channels; volatile scenarios will be refused)`);
76
+ this.set = new Set();
77
+ return;
78
+ }
79
+ this.set = new Set(doc.bindings.map((b) => this.key(b.surface, b.channelId)));
80
+ }
81
+ key(surface, channelId) {
82
+ // Unambiguous (JSON tuple) for the same reason canonicalBindingsPayload uses one —
83
+ // so a lookup key can never alias across the surface/channelId boundary. (Safe
84
+ // today since surface is a closed enum, but encoded explicitly for defense-in-depth.)
85
+ return JSON.stringify([surface, channelId]);
86
+ }
87
+ log(m) { this.d.logger?.(`[demo-channel-registry] ${m}`); }
88
+ /** True iff (surface, channelId) is a verified, registered demo channel. */
89
+ isDemoChannel(surface, channelId) {
90
+ return this.set.has(this.key(surface, channelId));
91
+ }
92
+ /** Whether a present bindings doc verified (false when none exist OR signature failed). */
93
+ get isVerified() { return this.verified; }
94
+ /** Count of registered demo channels (0 when unverified/absent). */
95
+ get size() { return this.set.size; }
96
+ }
97
+ //# sourceMappingURL=DemoChannelRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DemoChannelRegistry.js","sourceRoot":"","sources":["../../src/core/DemoChannelRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAkCH;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAA8C;IACrF,yEAAyE;IACzE,gFAAgF;IAChF,sEAAsE;IACtE,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,+EAA+E;IAC/E,eAAe;IACf,MAAM,IAAI,GAAG,CAAC,CAAqB,EAAE,EAAE,CACrC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC;IACnF,MAAM,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO;QACL,KAAK,GAAG,CAAC,OAAO,EAAE;QAClB,WAAW,GAAG,CAAC,SAAS,EAAE;QAC1B,YAAY,GAAG,CAAC,QAAQ,EAAE;QAC1B,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;KAC9B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,OAAO,mBAAmB;IACb,QAAQ,CAAU;IAClB,GAAG,CAAc;IACjB,CAAC,CAA0B;IAE5C,YAAY,IAA6B;QACvC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;QACd,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,+EAA+E;YAC/E,4EAA4E;YAC5E,gDAAgD;YAChD,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;YACtB,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QACD,MAAM,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,GAAG,GAAG,CAAC;QACvC,IAAI,EAAE,GAAG,KAAK,CAAC;QACf,IAAI,CAAC;YACH,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,wBAAwB,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC,CAAC;QACpE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,0CAA0C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACvG,EAAE,GAAG,KAAK,CAAC;QACb,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,kFAAkF;YAClF,IAAI,CAAC,GAAG,CAAC,4GAA4G,CAAC,CAAC;YACvH,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAChF,CAAC;IAEO,GAAG,CAAC,OAAgB,EAAE,SAAiB;QAC7C,mFAAmF;QACnF,+EAA+E;QAC/E,sFAAsF;QACtF,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;IAC9C,CAAC;IACO,GAAG,CAAC,CAAS,IAAU,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAEjF,4EAA4E;IAC5E,aAAa,CAAC,OAAgB,EAAE,SAAiB;QAC/C,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,2FAA2F;IAC3F,IAAI,UAAU,KAAc,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEnD,oEAAoE;IACpE,IAAI,IAAI,KAAa,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;CAC7C"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * RealChannelDriver — the production `ChannelDriver` for the LiveTestHarness
3
+ * (docs/specs/live-user-channel-proof-standard.md §5.4 "Platform-Sanctioned
4
+ * Automation"). It drives a feature end-to-end through the REAL user surfaces by
5
+ * composing one `SurfaceSender` per surface (Telegram, Slack, dashboard) plus:
6
+ *
7
+ * - a DemoChannelRegistry → `isDemoChannel` (§5.3 isolation), and
8
+ * - a `resolveResponderMachine` reader → the `responderMachineId` stamped on every
9
+ * reply, which is the DETERMINISTIC cross-machine proof the harness asserts on
10
+ * (e.g. "after the transfer, the reply was served FROM the Mini").
11
+ *
12
+ * The driver itself is pure transport composition: it knows nothing about HTTP,
13
+ * Telegram, or Slack — each surface sender + the placement reader are injected, so
14
+ * this module is fully unit-testable with fakes. The real senders (a demo-bot/user
15
+ * Telegram sender, a Slack user-token sender) and the real placement reader (a
16
+ * GET /pool/placement call) are wired at construction in server.ts.
17
+ *
18
+ * Safety: a surface with no registered sender is a hard error on use (never a silent
19
+ * skip that would fabricate a "no reply" FAIL), and `responderMachineId` resolution
20
+ * failures degrade to `undefined` (the harness then simply can't assert on responder)
21
+ * rather than throwing the whole scenario — the SEND/REPLY evidence is still recorded.
22
+ */
23
+ import type { ChannelDriver, SendResult, ReplyResult } from './LiveTestHarness.js';
24
+ import type { Surface } from './LiveTestArtifactStore.js';
25
+ import type { DemoChannelRegistry } from './DemoChannelRegistry.js';
26
+ /** One real surface transport. `channelId` is the surface-native id (topic id, Slack channel). */
27
+ export interface SurfaceSender {
28
+ /** Send a USER-role message on the real surface. */
29
+ send(channelId: string, text: string): Promise<SendResult>;
30
+ /** Await the agent's reply after `afterMessageId` (null on timeout). No responder id — the driver stamps that. */
31
+ awaitReply(channelId: string, opts: {
32
+ timeoutMs: number;
33
+ afterMessageId?: string;
34
+ }): Promise<Omit<ReplyResult, 'responderMachineId'> | null>;
35
+ }
36
+ export interface RealChannelDriverDeps {
37
+ /** Per-surface real senders. A surface absent here throws if a scenario targets it. */
38
+ senders: Partial<Record<Surface, SurfaceSender>>;
39
+ /** §5.3 demo-channel isolation. */
40
+ demoRegistry: Pick<DemoChannelRegistry, 'isDemoChannel'>;
41
+ /**
42
+ * Resolve WHICH machine served (owns) the given channel at reply time — the
43
+ * cross-machine proof. Returns the machine id (or nickname-resolvable id), or null
44
+ * if it can't be determined. MUST NOT throw on a transient read error (return null).
45
+ */
46
+ resolveResponderMachine: (surface: Surface, channelId: string) => Promise<string | null>;
47
+ logger?: (m: string) => void;
48
+ }
49
+ export declare class RealChannelDriver implements ChannelDriver {
50
+ private readonly d;
51
+ constructor(deps: RealChannelDriverDeps);
52
+ private log;
53
+ private senderFor;
54
+ isDemoChannel(surface: Surface, channelId: string): boolean;
55
+ send(surface: Surface, channelId: string, text: string): Promise<SendResult>;
56
+ awaitReply(surface: Surface, channelId: string, opts: {
57
+ timeoutMs: number;
58
+ afterMessageId?: string;
59
+ }): Promise<ReplyResult | null>;
60
+ }
61
+ //# sourceMappingURL=RealChannelDriver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RealChannelDriver.d.ts","sourceRoot":"","sources":["../../src/core/RealChannelDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,kGAAkG;AAClG,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3D,kHAAkH;IAClH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,oBAAoB,CAAC,GAAG,IAAI,CAAC,CAAC;CAC9I;AAED,MAAM,WAAW,qBAAqB;IACpC,uFAAuF;IACvF,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IACjD,mCAAmC;IACnC,YAAY,EAAE,IAAI,CAAC,mBAAmB,EAAE,eAAe,CAAC,CAAC;IACzD;;;;OAIG;IACH,uBAAuB,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzF,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qBAAa,iBAAkB,YAAW,aAAa;IACrD,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAwB;gBAC9B,IAAI,EAAE,qBAAqB;IAEvC,OAAO,CAAC,GAAG;IAEX,OAAO,CAAC,SAAS;IAUjB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAIrD,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAI5E,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;CAezI"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * RealChannelDriver — the production `ChannelDriver` for the LiveTestHarness
3
+ * (docs/specs/live-user-channel-proof-standard.md §5.4 "Platform-Sanctioned
4
+ * Automation"). It drives a feature end-to-end through the REAL user surfaces by
5
+ * composing one `SurfaceSender` per surface (Telegram, Slack, dashboard) plus:
6
+ *
7
+ * - a DemoChannelRegistry → `isDemoChannel` (§5.3 isolation), and
8
+ * - a `resolveResponderMachine` reader → the `responderMachineId` stamped on every
9
+ * reply, which is the DETERMINISTIC cross-machine proof the harness asserts on
10
+ * (e.g. "after the transfer, the reply was served FROM the Mini").
11
+ *
12
+ * The driver itself is pure transport composition: it knows nothing about HTTP,
13
+ * Telegram, or Slack — each surface sender + the placement reader are injected, so
14
+ * this module is fully unit-testable with fakes. The real senders (a demo-bot/user
15
+ * Telegram sender, a Slack user-token sender) and the real placement reader (a
16
+ * GET /pool/placement call) are wired at construction in server.ts.
17
+ *
18
+ * Safety: a surface with no registered sender is a hard error on use (never a silent
19
+ * skip that would fabricate a "no reply" FAIL), and `responderMachineId` resolution
20
+ * failures degrade to `undefined` (the harness then simply can't assert on responder)
21
+ * rather than throwing the whole scenario — the SEND/REPLY evidence is still recorded.
22
+ */
23
+ export class RealChannelDriver {
24
+ d;
25
+ constructor(deps) { this.d = deps; }
26
+ log(m) { this.d.logger?.(`[real-channel-driver] ${m}`); }
27
+ senderFor(surface) {
28
+ const s = this.d.senders[surface];
29
+ if (!s) {
30
+ // A missing sender is a CONFIGURATION error, surfaced loudly — never a silent
31
+ // skip that the harness would misread as a clean "no reply" FAIL.
32
+ throw new Error(`no real sender configured for surface "${surface}" — cannot drive it`);
33
+ }
34
+ return s;
35
+ }
36
+ isDemoChannel(surface, channelId) {
37
+ return this.d.demoRegistry.isDemoChannel(surface, channelId);
38
+ }
39
+ async send(surface, channelId, text) {
40
+ return this.senderFor(surface).send(channelId, text);
41
+ }
42
+ async awaitReply(surface, channelId, opts) {
43
+ const reply = await this.senderFor(surface).awaitReply(channelId, opts);
44
+ if (!reply)
45
+ return null;
46
+ let responderMachineId;
47
+ try {
48
+ responderMachineId = (await this.d.resolveResponderMachine(surface, channelId)) ?? undefined;
49
+ }
50
+ catch (err) {
51
+ // Degrade, never throw: we still have a real reply; we just can't attribute the
52
+ // responder machine this round. The harness records the reply evidence and
53
+ // (only) any responder-machine assertion can't be satisfied.
54
+ this.log(`responder-machine resolve failed for ${surface}:${channelId} (recording reply without it): ${err instanceof Error ? err.message : String(err)}`);
55
+ responderMachineId = undefined;
56
+ }
57
+ return { ...reply, responderMachineId };
58
+ }
59
+ }
60
+ //# sourceMappingURL=RealChannelDriver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RealChannelDriver.js","sourceRoot":"","sources":["../../src/core/RealChannelDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA4BH,MAAM,OAAO,iBAAiB;IACX,CAAC,CAAwB;IAC1C,YAAY,IAA2B,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAEnD,GAAG,CAAC,CAAS,IAAU,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAEvE,SAAS,CAAC,OAAgB;QAChC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,8EAA8E;YAC9E,kEAAkE;YAClE,MAAM,IAAI,KAAK,CAAC,0CAA0C,OAAO,qBAAqB,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,aAAa,CAAC,OAAgB,EAAE,SAAiB;QAC/C,OAAO,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAgB,EAAE,SAAiB,EAAE,IAAY;QAC1D,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAgB,EAAE,SAAiB,EAAE,IAAoD;QACxG,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACxE,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,kBAAsC,CAAC;QAC3C,IAAI,CAAC;YACH,kBAAkB,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,uBAAuB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC;QAC/F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,gFAAgF;YAChF,2EAA2E;YAC3E,6DAA6D;YAC7D,IAAI,CAAC,GAAG,CAAC,wCAAwC,OAAO,IAAI,SAAS,kCAAkC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC3J,kBAAkB,GAAG,SAAS,CAAC;QACjC,CAAC;QACD,OAAO,EAAE,GAAG,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC1C,CAAC;CACF"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * selfQuotaState — compute a machine's placement-eligibility block for the capacity
3
+ * heartbeat (spec: docs/specs/placement-llm-circuit-aware-quota.md).
4
+ *
5
+ * `quotaState.blocked` is the signal `PlacementExecutor` reads to decide whether a machine
6
+ * may serve a new LLM session. It is a **placement-eligibility** signal — "this machine
7
+ * cannot serve LLM work right now" — NOT a pure account-quota readout. Until now its only
8
+ * cause was account-quota exhaustion; an OPEN llm-circuit (the machine's provider calls are
9
+ * actually failing, rate-limited) is a second, more direct cause and MUST also block. The
10
+ * live test caught the gap: a machine with an open circuit reported `blocked:false` and
11
+ * placement routed a session onto it that died on arrival (2026-06-16, the Mac Mini).
12
+ *
13
+ * Extracted as a pure function so the two-signal contract (account-quota OR circuit) is
14
+ * unit-testable and cannot silently regress to quota-only.
15
+ */
16
+ export interface QuotaSnapshot {
17
+ blockedUntil?: string | null;
18
+ fiveHourPercent?: number | null;
19
+ blockReason?: string | null;
20
+ }
21
+ /**
22
+ * Type-level discriminator for the block CAUSE so consumers branch on a closed set, not a
23
+ * free string. `provider-block` / `five-hour-*` = account quota; `llm-circuit-open` =
24
+ * operational unavailability. Free-form legacy quota strings still flow through, typed as the
25
+ * open `string` arm (back-compat).
26
+ */
27
+ export type SelfQuotaBlockReason = 'llm-circuit-open' | 'five-hour-exhausted' | 'provider-block' | (string & {});
28
+ export interface SelfQuotaBlock {
29
+ blocked: boolean;
30
+ blockedUntil?: string;
31
+ reason?: SelfQuotaBlockReason;
32
+ }
33
+ /**
34
+ * Compute this machine's placement-eligibility block from BOTH the account-quota snapshot and
35
+ * the live llm-circuit availability.
36
+ *
37
+ * @param quota the account-quota snapshot (`quotaTracker.getState()`), or null/undefined when
38
+ * there is no tracker.
39
+ * @param circuitAvailable `llmCircuitAvailable()` — `true` when the breaker is disabled OR
40
+ * closed; `false` only when it is enabled AND open/half-open.
41
+ * @returns a block object, `{ blocked: false }`, or `undefined` (unknown ≠ blocked).
42
+ *
43
+ * Fail-open is preserved: an open circuit is the ONLY thing that newly blocks, and only on a
44
+ * *positively observed* unavailable circuit — missing information (no tracker + closed
45
+ * circuit) still returns `undefined`, which `PlacementExecutor` treats as not-blocked.
46
+ */
47
+ export declare function computeSelfQuotaState(quota: QuotaSnapshot | null | undefined, circuitAvailable: boolean, now?: number): SelfQuotaBlock | undefined;
48
+ //# sourceMappingURL=selfQuotaState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfQuotaState.d.ts","sourceRoot":"","sources":["../../src/core/selfQuotaState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAC5B,kBAAkB,GAClB,qBAAqB,GACrB,gBAAgB,GAChB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACvC,gBAAgB,EAAE,OAAO,EACzB,GAAG,GAAE,MAAmB,GACvB,cAAc,GAAG,SAAS,CAgB5B"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * selfQuotaState — compute a machine's placement-eligibility block for the capacity
3
+ * heartbeat (spec: docs/specs/placement-llm-circuit-aware-quota.md).
4
+ *
5
+ * `quotaState.blocked` is the signal `PlacementExecutor` reads to decide whether a machine
6
+ * may serve a new LLM session. It is a **placement-eligibility** signal — "this machine
7
+ * cannot serve LLM work right now" — NOT a pure account-quota readout. Until now its only
8
+ * cause was account-quota exhaustion; an OPEN llm-circuit (the machine's provider calls are
9
+ * actually failing, rate-limited) is a second, more direct cause and MUST also block. The
10
+ * live test caught the gap: a machine with an open circuit reported `blocked:false` and
11
+ * placement routed a session onto it that died on arrival (2026-06-16, the Mac Mini).
12
+ *
13
+ * Extracted as a pure function so the two-signal contract (account-quota OR circuit) is
14
+ * unit-testable and cannot silently regress to quota-only.
15
+ */
16
+ /**
17
+ * Compute this machine's placement-eligibility block from BOTH the account-quota snapshot and
18
+ * the live llm-circuit availability.
19
+ *
20
+ * @param quota the account-quota snapshot (`quotaTracker.getState()`), or null/undefined when
21
+ * there is no tracker.
22
+ * @param circuitAvailable `llmCircuitAvailable()` — `true` when the breaker is disabled OR
23
+ * closed; `false` only when it is enabled AND open/half-open.
24
+ * @returns a block object, `{ blocked: false }`, or `undefined` (unknown ≠ blocked).
25
+ *
26
+ * Fail-open is preserved: an open circuit is the ONLY thing that newly blocks, and only on a
27
+ * *positively observed* unavailable circuit — missing information (no tracker + closed
28
+ * circuit) still returns `undefined`, which `PlacementExecutor` treats as not-blocked.
29
+ */
30
+ export function computeSelfQuotaState(quota, circuitAvailable, now = Date.now()) {
31
+ // An open llm-circuit is a hard block regardless of the account-quota poll — the machine's
32
+ // provider calls are failing right now, so it cannot serve a session. This wins even when
33
+ // there is no quota snapshot (a machine with no tracker but an open circuit is still blocked).
34
+ if (!circuitAvailable)
35
+ return { blocked: true, reason: 'llm-circuit-open' };
36
+ if (!quota)
37
+ return undefined; // no tracker + circuit ok = unknown ≠ blocked
38
+ const blockActive = !!quota.blockedUntil && Date.parse(quota.blockedUntil) > now;
39
+ const fiveHourExhausted = (quota.fiveHourPercent ?? 0) >= 95;
40
+ if (!blockActive && !fiveHourExhausted)
41
+ return { blocked: false };
42
+ return {
43
+ blocked: true,
44
+ blockedUntil: quota.blockedUntil ?? undefined,
45
+ reason: quota.blockReason ??
46
+ (fiveHourExhausted ? `5-hour window at ${quota.fiveHourPercent}%` : 'provider block'),
47
+ };
48
+ }
49
+ //# sourceMappingURL=selfQuotaState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfQuotaState.js","sourceRoot":"","sources":["../../src/core/selfQuotaState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA0BH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAuC,EACvC,gBAAyB,EACzB,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,2FAA2F;IAC3F,0FAA0F;IAC1F,+FAA+F;IAC/F,IAAI,CAAC,gBAAgB;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC5E,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC,CAAC,8CAA8C;IAC5E,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC;IACjF,MAAM,iBAAiB,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7D,IAAI,CAAC,WAAW,IAAI,CAAC,iBAAiB;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAClE,OAAO;QACL,OAAO,EAAE,IAAI;QACb,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,SAAS;QAC7C,MAAM,EACJ,KAAK,CAAC,WAAW;YACjB,CAAC,iBAAiB,CAAC,CAAC,CAAC,oBAAoB,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC;KACxF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.593",
3
+ "version": "1.3.595",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-16T09:18:10.303Z",
5
- "instarVersion": "1.3.593",
4
+ "generatedAt": "2026-06-16T10:03:21.927Z",
5
+ "instarVersion": "1.3.595",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,35 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixed a multi-machine placement bug where a rate-limited machine could still be picked to serve
9
+ new conversations. The "can this machine take work?" health flag was computed only from the
10
+ slow account-usage poll and ignored the circuit breaker that trips when a machine's AI calls are
11
+ actually failing. So a machine with an open circuit advertised itself as available, and a new
12
+ conversation routed there died on arrival. Placement now treats an open (or recovering) circuit
13
+ as "blocked", so it steers new work to a machine that can actually answer. Caught by applying
14
+ the live-testing standard to the multi-machine transfer.
15
+
16
+ ## What to Tell Your User
17
+
18
+ If one of your machines is rate-limited, I now send new conversations to a machine that can
19
+ actually answer, instead of into a dead end where you'd get a "session stopped" notice. The only
20
+ time nothing improves is when every machine is rate-limited at once — then there is genuinely
21
+ nowhere good to send it, and I say so honestly. Single-machine setups are unaffected, and there
22
+ is no new setting to learn.
23
+
24
+ ## Summary of New Capabilities
25
+
26
+ | Capability | How to Use |
27
+ |-----------|-----------|
28
+ | Placement avoids a machine whose AI calls are currently failing | Automatic; a rate-limited machine shows as "blocked — llm-circuit-open" in the machines view and new conversations skip it |
29
+
30
+ ## Evidence
31
+
32
+ - 8 new unit tests (`tests/unit/selfQuotaState.test.ts`, both sides of every boundary) + a new
33
+ PlacementExecutor test asserting a circuit-open machine is skipped; tsc clean.
34
+ - Release gate: live two-machine re-run — a circuit-open machine reports blocked in the pool view
35
+ and a new conversation lands on a machine that can serve it.
@@ -0,0 +1,26 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Added the two pure core modules behind the `LiveTestHarness`'s `ChannelDriver` seam (spec: live-user-channel-proof-standard §5.3/§5.4):
9
+ - `DemoChannelRegistry` — verifies a signed demo-channel bindings doc; fail-closed `isDemoChannel(surface, channelId)` (absent or signature-invalid → zero demo channels). Canonical payload is an ordered JSON tuple so a signature can never be reused on a different binding set (closes a delimiter-collision bypass caught in second-pass review).
10
+ - `RealChannelDriver` — the production `ChannelDriver`: composes per-surface `SurfaceSender`s, dispatches send/awaitReply, and stamps `responderMachineId` on each reply via an injected placement reader (the deterministic cross-machine proof). Degrades-not-throws on a placement-read error; throws loudly on a missing surface sender (never a silent skip).
11
+
12
+ Ships DARK — not wired into server.ts. The real Telegram/Slack surface senders + the runner route are the next increment.
13
+
14
+ ## What to Tell Your User
15
+
16
+ Nothing yet — this is internal infrastructure for the gold-standard live-testing harness and ships dark (no runtime surface, no behavior change). The user-visible payoff lands when the harness runs end-to-end and proves a feature through the real Telegram/Slack channels before they ever test it.
17
+
18
+ ## Summary of New Capabilities
19
+
20
+ None user-facing in this increment. Internally: the building blocks that let the live-test harness drive real user channels and assert WHICH machine served a reply (the cross-machine seat-move proof). No new routes, no config, no flags flipped.
21
+
22
+ ## Evidence
23
+
24
+ - `tests/unit/DemoChannelRegistry.test.ts` — 9 tests: valid signed bindings resolve, absent → 0, tampered/forged/throwing-verify all fail-closed, cross-boundary signature-collision rejection, absent-vs-empty distinguishability, canonical order-independence + field-sensitivity.
25
+ - `tests/unit/RealChannelDriver.test.ts` — 7 tests: send dispatch, responderMachineId stamping, null-reply short-circuit, placement-error degrade, missing-sender loud throw, isDemoChannel delegation, null-responder.
26
+ - `tsc --noEmit` clean. instar-dev gate green (converged+approved spec, side-effects + second-pass review).
@@ -0,0 +1,41 @@
1
+ # Side-Effects Review — Live-Test Real Channel Driver core (DemoChannelRegistry + RealChannelDriver)
2
+
3
+ **Slug:** live-test-channel-drivers
4
+ **Spec:** docs/specs/live-user-channel-proof-standard.md §5.3 (Demo Channel Isolation) + §5.4 (Platform-Sanctioned Automation)
5
+ **Files:** src/core/DemoChannelRegistry.ts, src/core/RealChannelDriver.ts, tests/unit/DemoChannelRegistry.test.ts, tests/unit/RealChannelDriver.test.ts
6
+ **Posture:** ships DARK — two pure/injectable core modules, NOT yet wired into server.ts. No route, no runtime construction. The surface senders + runner route are the next increment (`.instar/plans/live-test-harness-drivers-BUILD.md`).
7
+
8
+ ## What it is
9
+ The production-side building blocks for the `LiveTestHarness`'s `ChannelDriver`:
10
+ - **DemoChannelRegistry** — verifies a SIGNED demo-channel bindings doc and answers `isDemoChannel(surface, channelId)`. Fail-closed: an absent OR signature-invalid doc grants ZERO demo channels.
11
+ - **RealChannelDriver** — composes one `SurfaceSender` per surface, dispatches `send`/`awaitReply`, and stamps `responderMachineId` on every reply via an injected `resolveResponderMachine` reader (the cross-machine proof the harness asserts on).
12
+
13
+ ## Phase 1 — Principle check (signal vs authority)
14
+ This increment touches a decision point — `isDemoChannel` gates whether a volatile/permission scenario may run on a given channel (§5.3). Analysis per `docs/signal-vs-authority.md`:
15
+ - **DemoChannelRegistry is a SIGNAL, not the authority.** It returns a boolean; the *authority* that refuses a run is the already-merged `LiveTestHarness.run` (it throws `HarnessVolatileChannelError`). The registry never blocks anything itself.
16
+ - The registry's logic is **fail-closed**, which is the safe direction for a §5.3 isolation check: a false "not a demo channel" only refuses a test; a false "is a demo channel" would let a destructive scenario touch the live operator channel. Verification failures (tampered/absent/throwing verify) all collapse to "no demo channels."
17
+ - **RealChannelDriver holds no decision authority** — pure transport composition. The one place it could mask a failure (a missing surface sender) throws LOUDLY rather than silently returning "no reply" (which the harness would misread as a clean FAIL).
18
+
19
+ Verdict: compliant. No brittle check with blocking authority added.
20
+
21
+ ## Phase 4 — Side-effects answers
22
+ 1. **Over-block** — `isDemoChannel` could return false for a channel that IS legitimately a demo channel if its signed bindings are absent/stale. Effect: a volatile scenario is refused (the harness throws) rather than running. That is the intended fail-closed direction — it never lets the scenario through, only ever refuses. Safe scenarios (the headline transfer-capstone) don't consult demo status, so they're unaffected.
23
+ 2. **Under-block** — the registry only vouches for channels in the signed list; it can't detect that a *non-demo* channel is secretly safe. By design — the harness simply requires volatile scenarios to be on a registered demo channel. No false "allow."
24
+ 3. **Level-of-abstraction fit** — correct layer: the registry is a pure data/verification helper feeding the harness gate (which already owns the §5.3 throw). The driver is the transport adapter the harness's injected-`ChannelDriver` seam was explicitly designed for (`LiveTestHarness.ts:8-11`). Neither reinvents a higher-layer gate.
25
+ 4. **Signal vs authority** — see Phase 1. Compliant (registry = fail-closed signal; harness = authority; driver = transport).
26
+ 5. **Interactions** — none yet: nothing constructs either class at runtime (dark). When wired, the driver composes existing senders + the `/pool/placement` reader; it does not shadow or double-fire any existing check. The registry reads a NEW state file (`state/demo-channel-bindings.json`) that no other component touches.
27
+ 6. **External surfaces** — none in this increment. No route, no message send, no other-agent-visible change. (The eventual senders WILL drive real Telegram/Slack — that surface lands in the next, separately-reviewed increment.)
28
+ 7. **Multi-machine posture** — `responderMachineId` is the feature's whole multi-machine point: the driver resolves WHICH machine served a reply (via the injected placement reader, which reads the authoritative `/pool/placement`, proxied to the lease-holder). The DemoChannelRegistry is **machine-local BY DESIGN** — demo-channel bindings are an operational/test-fixture fact of the machine running the harness; the harness runs on one machine at a time and asserts cross-machine behavior THROUGH the channel, not by replicating bindings. No silent single-machine assumption: the cross-machine dimension is carried explicitly by `responderMachineId`.
29
+ 8. **Rollback cost** — trivial: dark, unwired code. Back-out = revert the commit (or simply never wire it). No data migration, no live state, no fleet behavior change.
30
+
31
+ ## No-deferrals
32
+ The surface senders + runner are NOT a deferral of THIS increment — they are the next tracked increment (CMT-1568, `.instar/plans/live-test-harness-drivers-BUILD.md`). This increment is complete and self-contained: both modules are fully implemented (no stubs/TODOs) and fully unit-tested (16 tests, both sides of every boundary — valid/tampered/absent/throwing/collision/empty-vs-absent for the registry; dispatch/stamp/degrade/missing-sender for the driver).
33
+
34
+ ## Phase 5 — Second-pass review
35
+ An independent reviewer subagent audited the modules + this artifact adversarially.
36
+
37
+ **Concern raised (and RESOLVED):** `canonicalBindingsPayload`'s per-binding encoding was a delimiter-free concatenation (`${surface}${channelId}${workspaceId}${label}`), an ambiguous field boundary that lets a tampered binding collide to the same signed bytes — e.g. `{channelId:'C1', workspaceId:'W2'}` and `{channelId:'C1W2'}` serialize identically and share one valid signature, promoting an unvouched channelId to a demo channel. That is exactly the dangerous false "is demo" the module guards against.
38
+
39
+ **Fix applied:** the per-binding encoding is now an ordered JSON tuple (`JSON.stringify([surface, channelId, workspaceId ?? null, label ?? null])`), which makes every field boundary explicit and distinguishes an absent field (`null`) from a present empty string (`''`). The lookup `key()` was hardened the same way (defense-in-depth). Two regression tests added: a cross-boundary collision test (the tampered doc fails verification → the smuggled channel is NOT a demo channel) and an absent-vs-empty distinguishability test. (Also stripped stray `\x01` control bytes that had crept into the source — invisible and git-binary-fragile.)
40
+
41
+ Re-review verdict after fix: the collision is closed; the surface↔channelId boundary was already safe (surface is a closed enum, none a prefix of another); fail-closed paths and the driver's degrade-not-throw / loud-missing-sender behavior are correct. **Concur with the review (post-fix).**
@@ -0,0 +1,50 @@
1
+ # Side-Effects Review — Quota-aware placement must see the open LLM circuit
2
+
3
+ **Slug:** placement-llm-circuit-aware-quota
4
+ **Spec:** docs/specs/placement-llm-circuit-aware-quota.md
5
+ **Parent principle:** No Silent Degradation to a Brittle Fallback — a machine that cannot serve LLM work must report itself blocked, not present a healthy-looking signal that misroutes work onto it.
6
+
7
+ ## What changed
8
+
9
+ `selfQuotaState()` (the capacity-heartbeat block `PlacementExecutor` reads) derived `blocked`
10
+ ONLY from the account-quota poll, ignoring the `LlmCircuitBreaker`. So a machine whose CLI was
11
+ actually rate-limited (circuit OPEN) could still report `quotaState:{blocked:false}` and
12
+ placement would route a session onto it that dies on arrival. Caught live (2026-06-16, the Mac
13
+ Mini) by the gold-standard live test of the multi-machine transfer.
14
+
15
+ Fix: extract `computeSelfQuotaState(quota, circuitAvailable)` into `src/core/selfQuotaState.ts`
16
+ (pure, unit-tested), OR-ing an open/half-open circuit (`!llmCircuitAvailable()`) into the block.
17
+ `server.ts` calls it with the live quota snapshot AND `llmCircuitAvailable()`.
18
+
19
+ ## Blast radius
20
+
21
+ - **Multi-machine placement (the target):** a circuit-open machine is now excluded from
22
+ placement; new sessions land on a machine that can serve them. This was BROKEN before — net
23
+ improvement, strictly better whenever ANY machine is available.
24
+ - **All-machines-circuit-open:** identical to today's all-account-quota-blocked case —
25
+ placement falls back to least-loaded and flags `all-machines-quota-blocked` (now possibly
26
+ carrying `llm-circuit-open` reasons). Not a regression; nowhere good to route by definition.
27
+ - **Single machine:** no peers to compare; the existing all-blocked least-loaded fallback
28
+ applies, unchanged.
29
+ - **Fail-open preserved:** only a positively-observed open circuit newly blocks. Missing info
30
+ (no tracker + closed circuit, or any throw) → `undefined` = unknown ≠ blocked, exactly as
31
+ before. A DISABLED breaker reports available → never a false block.
32
+ - **Wire field:** `quotaState` keeps its name (a replicated heartbeat field consumed
33
+ cross-machine + by the dashboard; renaming is a separate wire migration). Its MEANING is
34
+ pinned by the doc contract + a new `SelfQuotaBlockReason` enum (`llm-circuit-open` |
35
+ `five-hour-exhausted` | `provider-block` | string). No consumer keys on a specific reason
36
+ string, so widening the cause set is non-breaking.
37
+
38
+ ## Reversibility
39
+
40
+ Governed by the existing `intelligence.circuitBreaker` config — disabling the breaker (or it
41
+ never tripping) keeps `llmCircuitAvailable()` true so this never blocks. Revertable by reverting
42
+ the two-file diff; no durable state is written (the block is recomputed per heartbeat).
43
+
44
+ ## Risk / monitoring
45
+
46
+ Low. Pure-function change off the hot path (computed per 30s heartbeat). Observable via
47
+ `GET /pool` (`quotaState.reason: 'llm-circuit-open'`) + the existing placement decision flags +
48
+ the already-audited `[llm-circuit]` transitions in `logs/server.log`. Release gate: the live
49
+ two-machine re-run — a circuit-open machine shows `blocked:true` in `/pool` and a new
50
+ conversation lands on a machine that can serve it.