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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +9 -13
- package/dist/commands/server.js.map +1 -1
- package/dist/core/DemoChannelRegistry.d.ts +73 -0
- package/dist/core/DemoChannelRegistry.d.ts.map +1 -0
- package/dist/core/DemoChannelRegistry.js +97 -0
- package/dist/core/DemoChannelRegistry.js.map +1 -0
- package/dist/core/RealChannelDriver.d.ts +61 -0
- package/dist/core/RealChannelDriver.d.ts.map +1 -0
- package/dist/core/RealChannelDriver.js +60 -0
- package/dist/core/RealChannelDriver.js.map +1 -0
- package/dist/core/selfQuotaState.d.ts +48 -0
- package/dist/core/selfQuotaState.d.ts.map +1 -0
- package/dist/core/selfQuotaState.js +49 -0
- package/dist/core/selfQuotaState.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.594.md +35 -0
- package/upgrades/1.3.595.md +26 -0
- package/upgrades/side-effects/live-test-channel-drivers.md +41 -0
- package/upgrades/side-effects/placement-llm-circuit-aware-quota.md +50 -0
|
@@ -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":"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"}
|
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
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.
|