@henols/vice-mcp 0.2.3 → 0.2.4
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/anno-cli.ts +156 -158
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-export-asm.ts +100 -101
- package/anno-graphics.ts +16 -16
- package/anno-hazard-report.ts +2 -2
- package/anno-import.ts +15 -15
- package/anno-index.ts +8 -8
- package/anno-join.ts +35 -35
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +4 -4
- package/anno-regbits-gen.ts +13 -13
- package/anno-store-export.ts +11 -11
- package/anno-store.ts +139 -144
- package/anno-symbols.ts +7 -7
- package/anno-types.ts +55 -55
- package/package.json +1 -1
- package/resources/broker-control.mjs +85 -92
- package/resources/broker-epoch.mjs +6 -7
- package/resources/broker-kill.mjs +29 -30
- package/resources/broker-launch.mjs +352 -370
- package/resources/broker-state.mjs +9 -10
- package/resources/host-tool.mjs +636 -664
- package/resources/vice-broker.mjs +189 -191
- package/vice-broker-client.ts +98 -100
package/vice-broker-client.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Container-side half of the on-demand broker protocol.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Container-side half of the on-demand broker protocol. This module used to
|
|
3
|
+
// write the request/lease files resources/vice-broker.sh read and read the
|
|
4
|
+
// grant/denial/broker files that script wrote, all on the SAME
|
|
5
5
|
// .vice-supervisor/ bind mount tools/vice-supervisor.sh's epoch.json already
|
|
6
|
-
// used.
|
|
7
|
-
//
|
|
6
|
+
// used. That whole file protocol is now deleted wholesale (six mechanisms
|
|
7
|
+
// retiring together -- startHeartbeat()/the mtime-as-heartbeat
|
|
8
8
|
// convention/touchLease()/pollGrant()/pollRecycleAck()/the request-grant-
|
|
9
9
|
// denial-lease-recycle-ack directory tree) now that vice-proxy.ts's
|
|
10
10
|
// acquisition, release AND recycle all run over the TCP control plane
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
//
|
|
18
18
|
// Every read of broker.json is still untrusted input: parse in try/catch, a
|
|
19
19
|
// malformed or half-written file is "not there yet" or "absent", never a
|
|
20
|
-
// thrown exception
|
|
21
|
-
//
|
|
20
|
+
// thrown exception -- the never-throw, never-cache-a-negative-result
|
|
21
|
+
// posture this module holds throughout.
|
|
22
22
|
//
|
|
23
23
|
// MUST NOT import hostpath.ts: the host-path consumer set is closed to
|
|
24
24
|
// exactly four production modules (containerpath.ts, install-resources.ts,
|
|
@@ -31,14 +31,15 @@ import { join, resolve } from "node:path";
|
|
|
31
31
|
import { connect, type Socket } from "node:net";
|
|
32
32
|
|
|
33
33
|
import { supervisorDir } from "./repo-root.ts";
|
|
34
|
-
//
|
|
34
|
+
// TYPE-ONLY, and IMPORTED rather than
|
|
35
35
|
// redeclared. broker-launch.mts is the one definition of the profile shape and
|
|
36
36
|
// the module that turns a profile into argv; a second local shape here is how
|
|
37
37
|
// a client would start requesting a knob the host cannot honour. Type-only, so
|
|
38
38
|
// the container-side bundle never resolves the host-bound module at runtime.
|
|
39
39
|
import type { LaunchProfile } from "./broker-launch.mts";
|
|
40
40
|
// backend-detect.mts is ViceBackend's one home (narrowed to a single literal
|
|
41
|
-
//
|
|
41
|
+
// now that the fork backend has been removed entirely). Type-only, same
|
|
42
|
+
// discipline as the import above.
|
|
42
43
|
import type { ViceBackend } from "./backend-detect.mts";
|
|
43
44
|
// The module tree's ONE definition of the container-visible host alias
|
|
44
45
|
// (vice.ts:49), carrying its own VICE_MCP_HOST override -- consumed below by
|
|
@@ -57,8 +58,8 @@ import { mcpHost, ViceError } from "./vice-errors.ts";
|
|
|
57
58
|
|
|
58
59
|
// -------------------------------------------------------------- request ids
|
|
59
60
|
//
|
|
60
|
-
// Primary noun of this protocol
|
|
61
|
-
//
|
|
61
|
+
// Primary noun of this protocol: a request/grant/lease is identified by
|
|
62
|
+
// this id, never by port -- ports are
|
|
62
63
|
// recycled across sessions under on-demand launch, so a port is an attribute
|
|
63
64
|
// OF a grant, not identity. Matched byte-for-byte against the same shape
|
|
64
65
|
// resources/vice-broker.sh's own request-id pattern validates (T-01.2-01);
|
|
@@ -66,12 +67,10 @@ import { mcpHost, ViceError } from "./vice-errors.ts";
|
|
|
66
67
|
// shared corpus through both validators so neither side can silently accept
|
|
67
68
|
// an id shape the other rejects.
|
|
68
69
|
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// re-stating the pattern a third time; the bash copy
|
|
74
|
-
// (resources/vice-broker.sh) does not retire until that phase deletes it.
|
|
70
|
+
// This is a real, typed, NAMED export whose VALUE is unchanged from
|
|
71
|
+
// the pre-conversion .mjs (verified live). The in-process broker imports
|
|
72
|
+
// this exact binding rather than re-stating the pattern a third time; the
|
|
73
|
+
// bash copy (resources/vice-broker.sh) does not retire until it is deleted.
|
|
75
74
|
export const REQUEST_ID_PATTERN: RegExp = /^req-[0-9]+-[0-9]+-[0-9a-f]{8}$/;
|
|
76
75
|
|
|
77
76
|
export function newRequestId(): string {
|
|
@@ -91,8 +90,9 @@ export function isValidRequestId(id: unknown): id is string {
|
|
|
91
90
|
// The five sibling directory helpers this function used to anchor
|
|
92
91
|
// (requestsDir/grantsDir/denialsDir/brokerLeasesDir/recycleAcksDir) and the
|
|
93
92
|
// lease path helper (leasePathFor) are GONE, not merely unused -- their
|
|
94
|
-
// directories cease to exist
|
|
95
|
-
// survives, since broker.json itself is not part of
|
|
93
|
+
// directories cease to exist now that the file protocol is retired; only
|
|
94
|
+
// brokerJsonPath() below survives, since broker.json itself is not part of
|
|
95
|
+
// the retiring protocol.
|
|
96
96
|
export function brokerRootDir(): string {
|
|
97
97
|
return process.env.VICE_POOL_DIR ? resolve(process.env.VICE_POOL_DIR) : supervisorDir();
|
|
98
98
|
}
|
|
@@ -134,7 +134,7 @@ function readJsonMaybe(path: string): Record<string, unknown> | null {
|
|
|
134
134
|
// writeRequest/createLease/touchLease/releaseLease/pollGrant/pollRecycleAck
|
|
135
135
|
// and their record interfaces (RequestRecord, RecycleRequestRecord,
|
|
136
136
|
// LeaseRecord, PollOptions, PollGrantResult, PollRecycleAckResult) are GONE:
|
|
137
|
-
// the whole file-messaging protocol they implemented
|
|
137
|
+
// the whole file-messaging protocol they implemented is replaced
|
|
138
138
|
// wholesale by the TCP control plane below. GRANT_POLL_TIMEOUT_MS/
|
|
139
139
|
// GRANT_POLL_INTERVAL_MS/RECYCLE_ACK_TIMEOUT_MS/RECYCLE_ACK_POLL_INTERVAL_MS
|
|
140
140
|
// (the retiring polls' own timeout/interval constants) and sleepMs() (their
|
|
@@ -183,8 +183,8 @@ export function readBrokerLiveness(path: string = brokerJsonPath()): BrokerLiven
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// StartHeartbeatOptions/HEARTBEAT_MS/startHeartbeat() are GONE -- the
|
|
186
|
-
// lease-heartbeat interval (one of
|
|
187
|
-
// successor. Nothing needs touching to prove a TCP connection is alive; it
|
|
186
|
+
// lease-heartbeat interval (one of the six retiring mechanisms named above)
|
|
187
|
+
// has no successor. Nothing needs touching to prove a TCP connection is alive; it
|
|
188
188
|
// either is, or the broker's own "close" handler has already reclaimed the
|
|
189
189
|
// instance.
|
|
190
190
|
|
|
@@ -300,9 +300,8 @@ export function resolveControlTarget(record: Record<string, unknown>, port: numb
|
|
|
300
300
|
// ---------------------------------------------------- TCP control plane
|
|
301
301
|
//
|
|
302
302
|
// The container-side half of the TCP control plane (broker-control.mts is
|
|
303
|
-
// the host-side half). Wire format confirmed at
|
|
304
|
-
//
|
|
305
|
-
// .planning/RE-FINDINGS.md for the full record): newline-delimited JSON,
|
|
303
|
+
// the host-side half). Wire format confirmed at an early blocking
|
|
304
|
+
// decision checkpoint (2026-08-03, `as-specified`): newline-delimited JSON,
|
|
306
305
|
// per-boot capability token, connection open = claim / close = release.
|
|
307
306
|
export interface AcquireGrant {
|
|
308
307
|
id: string;
|
|
@@ -310,15 +309,16 @@ export interface AcquireGrant {
|
|
|
310
309
|
url: string;
|
|
311
310
|
epoch_file: string;
|
|
312
311
|
supervisor_dir: string;
|
|
313
|
-
/**
|
|
314
|
-
*
|
|
312
|
+
/** The broker-allocated port stock's `-remotemonitor` text monitor binds,
|
|
313
|
+
* mandatory in fact once a stock acquire without one was made to fail
|
|
314
|
+
* outright rather than degrade.
|
|
315
315
|
* Absent on a fork grant only -- a stock grant that could not bind a
|
|
316
316
|
* text-monitor port no longer reaches the wire at all: the acquire fails
|
|
317
317
|
* outright (`no_free_text_port`) before any grant is produced. */
|
|
318
318
|
remote_monitor_port?: number;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
/**
|
|
321
|
+
/** Parses the wire's `remote_monitor_port` into a validated
|
|
322
322
|
* integer in 1..65535, or `undefined` when the key is absent OR the observed
|
|
323
323
|
* value is not a valid port -- never a fabricated 0/null standing in for
|
|
324
324
|
* "no port", and never an unvalidated number handed downstream to a dial.
|
|
@@ -338,7 +338,7 @@ export interface AcquireOverControlPlaneHandle {
|
|
|
338
338
|
release: () => void;
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
/**
|
|
341
|
+
/** Default raised from 25000 to 120000. The
|
|
342
342
|
* knob (VICE_BROKER_ACQUIRE_TIMEOUT_MS) is unchanged -- an explicitly
|
|
343
343
|
* configured value keeps working exactly as before.
|
|
344
344
|
*
|
|
@@ -354,7 +354,7 @@ export interface AcquireOverControlPlaneHandle {
|
|
|
354
354
|
* client's generic timeout. */
|
|
355
355
|
export const CONTROL_ACQUIRE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER_ACQUIRE_TIMEOUT_MS || 120000);
|
|
356
356
|
|
|
357
|
-
/**
|
|
357
|
+
/** The request-side launch profile.
|
|
358
358
|
* Optional and absent by default at BOTH acquire write sites in this file.
|
|
359
359
|
*
|
|
360
360
|
* ONE RULE, and it is the whole reason this shape is named rather than
|
|
@@ -373,17 +373,18 @@ export const CONTROL_ACQUIRE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER
|
|
|
373
373
|
* one of them silently never arrives for callers on the other path, which is
|
|
374
374
|
* the same defect class as a tool argument that is accepted and dropped.
|
|
375
375
|
*
|
|
376
|
-
* CONSUMER STATUS: SUBSTRATE, NOT YET WIRED
|
|
376
|
+
* CONSUMER STATUS: SUBSTRATE, NOT YET WIRED. The profile
|
|
377
377
|
* threads client -> wire -> narrowing -> eligibility -> argv -> record with
|
|
378
|
-
* tests at every hop, but
|
|
378
|
+
* tests at every hop, but NO production call site passes one yet:
|
|
379
379
|
* `acquireOverControlPlane()` and `BrokerControlSession.acquire()` are only
|
|
380
380
|
* ever invoked without `opts.profile`, so `-warp` and `-console` are
|
|
381
|
-
* unreachable in production. That is deliberate --
|
|
382
|
-
* ahead of
|
|
383
|
-
* a reader to discover, because a fully-tested chain reads as a
|
|
381
|
+
* unreachable in production. That is deliberate -- the chain was built
|
|
382
|
+
* ahead of the callers that will use it -- and is recorded here rather than
|
|
383
|
+
* left for a reader to discover, because a fully-tested chain reads as a
|
|
384
|
+
* live one.
|
|
384
385
|
*
|
|
385
386
|
* Do NOT close this by inventing a call site. Note also that the profile is
|
|
386
|
-
* refused outright on the fork backend (
|
|
387
|
+
* refused outright on the fork backend (broker-control.mts):
|
|
387
388
|
* it maps to stock-only launch flags, so the first real consumer has to be on
|
|
388
389
|
* stock. */
|
|
389
390
|
export interface AcquireProfileOptions {
|
|
@@ -405,7 +406,7 @@ function acquireProfileFragment(profile?: LaunchProfile): { profile?: LaunchProf
|
|
|
405
406
|
* failure: broker.json absent/unreadable/missing the control fields, a
|
|
406
407
|
* connection error, an `error` response, or a timeout.
|
|
407
408
|
*
|
|
408
|
-
*
|
|
409
|
+
* Additionally takes an optional `profile` (see
|
|
409
410
|
* AcquireProfileOptions above). Omitting it writes the exact wire line this
|
|
410
411
|
* function has always written. */
|
|
411
412
|
export function acquireOverControlPlane(dir: string = brokerRootDir(), opts: AcquireProfileOptions = {}): Promise<AcquireOverControlPlaneHandle> {
|
|
@@ -444,7 +445,7 @@ export function acquireOverControlPlane(dir: string = brokerRootDir(), opts: Acq
|
|
|
444
445
|
|
|
445
446
|
socket.on("connect", () => {
|
|
446
447
|
const requestId = newRequestId();
|
|
447
|
-
//
|
|
448
|
+
// Write site ONE of two (see
|
|
448
449
|
// acquireProfileFragment()'s own comment) -- the key is absent entirely
|
|
449
450
|
// when no profile was requested, so this line stays byte-identical to
|
|
450
451
|
// what it always was for a profile-less acquire.
|
|
@@ -514,16 +515,16 @@ export function acquireOverControlPlane(dir: string = brokerRootDir(), opts: Acq
|
|
|
514
515
|
}
|
|
515
516
|
|
|
516
517
|
// ---------------------------------------------------------------------------
|
|
517
|
-
// BROKER-CONTROL-CLIENT REGION START
|
|
518
|
+
// BROKER-CONTROL-CLIENT REGION START
|
|
518
519
|
//
|
|
519
|
-
//
|
|
520
|
-
// openBrokerControl() is the container-side half of
|
|
521
|
-
// all five request kinds, one discovery-record read, real
|
|
522
|
-
// deadlines, and a distinct broker-gone outcome. Lives alongside
|
|
523
|
-
// acquireOverControlPlane() above (
|
|
520
|
+
// The file protocol beside it is now gone.
|
|
521
|
+
// openBrokerControl() is the container-side half of the TCP control plane:
|
|
522
|
+
// session shape, all five request kinds, one discovery-record read, real
|
|
523
|
+
// per-request deadlines, and a distinct broker-gone outcome. Lives alongside
|
|
524
|
+
// acquireOverControlPlane() above (an early tracer, kept unchanged and
|
|
524
525
|
// still used by broker-e2e.test.ts/broker-kill.test.ts as their own one-shot
|
|
525
526
|
// acquire helper for exercising the SERVER side) -- the file protocol this
|
|
526
|
-
// region's own predecessor sat beside is gone
|
|
527
|
+
// region's own predecessor sat beside is gone entirely.
|
|
527
528
|
//
|
|
528
529
|
// Deliberately never REJECTS a promise: every failure -- deadline, a
|
|
529
530
|
// refused connection, a malformed line, the broker going away mid-request --
|
|
@@ -541,21 +542,19 @@ export function acquireOverControlPlane(dir: string = brokerRootDir(), opts: Acq
|
|
|
541
542
|
|
|
542
543
|
/** Same value as the tracer's own CONTROL_ACQUIRE_TIMEOUT_MS above --
|
|
543
544
|
* referenced directly (not re-computed from the env var a second time) so
|
|
544
|
-
* the two can never drift apart. This is
|
|
545
|
-
* retiring grant-poll timeout
|
|
546
|
-
*
|
|
547
|
-
* from 25000 per P-08 / 01.6.2.1-04-PLAN.md, against the measured tool-call
|
|
545
|
+
* the two can never drift apart. This is the relocated value of the
|
|
546
|
+
* retiring grant-poll timeout (VICE_BROKER_ACQUIRE_TIMEOUT_MS, default now
|
|
547
|
+
* 120000, raised from 25000 against the measured tool-call
|
|
548
548
|
* budget spike-003 established -- see the counter-evidence comment at
|
|
549
549
|
* CONTROL_ACQUIRE_TIMEOUT_MS's own declaration above). */
|
|
550
550
|
export const ACQUIRE_TIMEOUT_MS: number = CONTROL_ACQUIRE_TIMEOUT_MS;
|
|
551
551
|
|
|
552
|
-
/** The recycle bound.
|
|
553
|
-
* pollRecycleAck()'s own RECYCLE_ACK_TIMEOUT_MS directly, so the
|
|
554
|
-
* never drift apart while both existed; that predecessor is gone
|
|
555
|
-
*
|
|
556
|
-
* itself is unchanged (VICE_BROKER_RECYCLE_TIMEOUT_MS, default
|
|
557
|
-
*
|
|
558
|
-
* Phase 01.6.2.1's item. */
|
|
552
|
+
/** The recycle bound. An earlier revision referenced the (now-deleted)
|
|
553
|
+
* retiring pollRecycleAck()'s own RECYCLE_ACK_TIMEOUT_MS directly, so the
|
|
554
|
+
* two could never drift apart while both existed; that predecessor is gone
|
|
555
|
+
* entirely, so this reads the SAME environment variable directly -- the
|
|
556
|
+
* value itself is unchanged (VICE_BROKER_RECYCLE_TIMEOUT_MS, default
|
|
557
|
+
* 30000). Final tuning remains a future item. */
|
|
559
558
|
export const RECYCLE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER_RECYCLE_TIMEOUT_MS || 30000);
|
|
560
559
|
|
|
561
560
|
/** Genuinely NEW: the file protocol never "connected" anywhere, so there is
|
|
@@ -563,9 +562,9 @@ export const RECYCLE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER_RECYCLE
|
|
|
563
562
|
* a TCP connect over the docker bridge to a broker broker.json has already
|
|
564
563
|
* classified alive (never_started/stale are refused before a connection is
|
|
565
564
|
* ever attempted) -- deliberately not read from an environment variable,
|
|
566
|
-
* since
|
|
567
|
-
*
|
|
568
|
-
*
|
|
565
|
+
* since no new environment variable was wanted beyond the two deadline
|
|
566
|
+
* variables named above. Final tuning remains a future item, same as the
|
|
567
|
+
* other two. */
|
|
569
568
|
export const CONTROL_CONNECT_TIMEOUT_MS = 5000;
|
|
570
569
|
|
|
571
570
|
/** Every way a session-level request can fail to produce its expected
|
|
@@ -592,7 +591,7 @@ export type ControlFailureKind =
|
|
|
592
591
|
| "no_free_port"
|
|
593
592
|
| "at_capacity"
|
|
594
593
|
| "internal"
|
|
595
|
-
//
|
|
594
|
+
// The broker's own ControlErrorCode gained
|
|
596
595
|
// this member for the ownership-conflict outcome; duplicated here for the
|
|
597
596
|
// same reason every other member already is (this client and the broker
|
|
598
597
|
// run in separate processes -- the shared surface is the wire format, not
|
|
@@ -631,7 +630,7 @@ interface ControlHostStateFields {
|
|
|
631
630
|
warm_floor: number;
|
|
632
631
|
max_instances: number;
|
|
633
632
|
base_port: number;
|
|
634
|
-
/**
|
|
633
|
+
/** Narrowed from `"fork" | "stock" | null` to
|
|
635
634
|
* `ViceBackend | null` -- `null` when the broker predates this field or
|
|
636
635
|
* sent something unrecognised: absent evidence, kept distinct from a
|
|
637
636
|
* definite value. text-tools.ts's own broker-identity cross-check (out of
|
|
@@ -644,7 +643,7 @@ export type ControlHostStateResult =
|
|
|
644
643
|
| { ok: false; kind: ControlFailureKind; message: string };
|
|
645
644
|
|
|
646
645
|
// ---------------------------------------------------------------------------
|
|
647
|
-
// MonitorClaimChannel
|
|
646
|
+
// MonitorClaimChannel: the two-value channel contract,
|
|
648
647
|
// declared HERE as a local literal union rather than imported from
|
|
649
648
|
// broker-state.mts -- that module is host-bound and compiled into
|
|
650
649
|
// resources/*.mjs, and this file is the container-side half. The shared
|
|
@@ -655,9 +654,8 @@ export type ControlHostStateResult =
|
|
|
655
654
|
// ---------------------------------------------------------------------------
|
|
656
655
|
export type MonitorClaimChannel = "binary" | "text";
|
|
657
656
|
|
|
658
|
-
/**
|
|
659
|
-
*
|
|
660
|
-
* refusal -- field-for-field the same shape the broker's own MonitorHolder
|
|
657
|
+
/** The current monitor-socket holder's own identity, named in a
|
|
658
|
+
* `monitor_owned` refusal -- field-for-field the same shape the broker's own MonitorHolder
|
|
661
659
|
* carries (broker-control.mts), minus nothing (pid included, matching
|
|
662
660
|
* GrantRecord's own convention this whole mechanism mirrors). */
|
|
663
661
|
export interface MonitorClaimHolder {
|
|
@@ -670,8 +668,8 @@ export interface MonitorClaimHolder {
|
|
|
670
668
|
export interface ClaimMonitorOptions {
|
|
671
669
|
targetId: string;
|
|
672
670
|
timeoutMs?: number;
|
|
673
|
-
/**
|
|
674
|
-
* byte-identical to `"binary"` -- every pre-
|
|
671
|
+
/** Which monitor socket to claim. Omitted is
|
|
672
|
+
* byte-identical to `"binary"` -- every pre-existing call site (and every
|
|
675
673
|
* broker that predates this field) keeps working unchanged. */
|
|
676
674
|
channel?: MonitorClaimChannel;
|
|
677
675
|
}
|
|
@@ -679,20 +677,20 @@ export interface ClaimMonitorOptions {
|
|
|
679
677
|
export interface ReleaseMonitorOptions {
|
|
680
678
|
targetId: string;
|
|
681
679
|
timeoutMs?: number;
|
|
682
|
-
/**
|
|
680
|
+
/** Same default-to-binary posture as ClaimMonitorOptions.channel. */
|
|
683
681
|
channel?: MonitorClaimChannel;
|
|
684
682
|
}
|
|
685
683
|
|
|
686
|
-
/** Discriminated claim outcome
|
|
684
|
+
/** Discriminated claim outcome: `monitor_owned` is kept STRICTLY
|
|
687
685
|
* separate from `timeout` -- conflating "someone else holds it" with "the
|
|
688
|
-
* broker did not answer" would reintroduce exactly the ambiguity
|
|
689
|
-
* exists to remove. Never thrown; a caller that wants to raise instead
|
|
686
|
+
* broker did not answer" would reintroduce exactly the ambiguity this
|
|
687
|
+
* distinction exists to remove. Never thrown; a caller that wants to raise instead
|
|
690
688
|
* should construct a MonitorOwnershipError from this outcome's own fields
|
|
691
689
|
* (see that class's own header comment). */
|
|
692
690
|
export type ClaimMonitorOutcome =
|
|
693
691
|
| { ok: true }
|
|
694
692
|
| { ok: false; reason: "monitor_owned"; holder: MonitorClaimHolder }
|
|
695
|
-
// "denied"
|
|
693
|
+
// "denied": the broker's control plane refused because the grant
|
|
696
694
|
// named is not the one THIS connection holds. In a correct client that is
|
|
697
695
|
// unreachable -- stockConnect() always claims the grant its own session
|
|
698
696
|
// acquired -- so it is carried as its own reason rather than collapsed into
|
|
@@ -707,15 +705,15 @@ export interface MonitorOwnershipErrorOptions {
|
|
|
707
705
|
holderGrantId?: string;
|
|
708
706
|
holderClaimedAt?: number;
|
|
709
707
|
port?: number;
|
|
710
|
-
/**
|
|
708
|
+
/** Which socket is contended -- so a handshake failure
|
|
711
709
|
* can say which channel was refused without re-parsing the message. */
|
|
712
710
|
channel?: MonitorClaimChannel;
|
|
713
711
|
}
|
|
714
712
|
|
|
715
713
|
/** Thrown (by a caller that prefers to raise rather than branch on
|
|
716
714
|
* ClaimMonitorOutcome) when `monitor_claim` is refused because a DIFFERENT
|
|
717
|
-
* grant already holds this instance's monitor socket
|
|
718
|
-
*
|
|
715
|
+
* grant already holds this instance's monitor socket. Names the holding
|
|
716
|
+
* grant and the port plainly, as an ownership
|
|
719
717
|
* conflict -- a state the broker itself enforced, distinct from an emulator
|
|
720
718
|
* that has stopped answering, and NOT a state the vice-wedge-triage skill's
|
|
721
719
|
* opening move should ever be misdirected by.
|
|
@@ -724,9 +722,9 @@ export interface MonitorOwnershipErrorOptions {
|
|
|
724
722
|
* connect() is ever attempted: stock VICE services exactly one binmon
|
|
725
723
|
* client, and a second connect() produces no reply and no EOF, so a refusal
|
|
726
724
|
* arriving only after dialling would be byte-for-byte indistinguishable
|
|
727
|
-
* from a wedge
|
|
725
|
+
* from a wedge. Claiming first means this refusal is a JSON
|
|
728
726
|
* response on a control-plane socket that already works, and the second
|
|
729
|
-
* client never dials the binmon port at all
|
|
727
|
+
* client never dials the binmon port at all. */
|
|
730
728
|
export class MonitorOwnershipError extends ViceError {
|
|
731
729
|
holderGrantId?: string;
|
|
732
730
|
holderClaimedAt?: number;
|
|
@@ -759,16 +757,16 @@ export interface ControlDeadlineOptions {
|
|
|
759
757
|
* exactly one request line and resolves against its own deadline; none of
|
|
760
758
|
* them ever reject. */
|
|
761
759
|
export interface BrokerControlSession {
|
|
762
|
-
/**
|
|
760
|
+
/** Additionally takes an optional
|
|
763
761
|
* `profile` -- see AcquireProfileOptions. Omitting it is byte-identical to
|
|
764
|
-
* every pre-
|
|
762
|
+
* every pre-existing call. */
|
|
765
763
|
acquire(opts?: ControlDeadlineOptions & AcquireProfileOptions): Promise<ControlAcquireResult>;
|
|
766
764
|
release(): Promise<ControlReleaseResult>;
|
|
767
765
|
recycle(targetId: string, opts?: ControlDeadlineOptions): Promise<ControlRecycleResult>;
|
|
768
766
|
status(opts?: ControlDeadlineOptions): Promise<ControlStatusResult>;
|
|
769
767
|
hostState(opts?: ControlDeadlineOptions): Promise<ControlHostStateResult>;
|
|
770
768
|
/** Claims exclusive ownership of an instance's monitor socket BEFORE any
|
|
771
|
-
* binmon connect() is attempted
|
|
769
|
+
* binmon connect() is attempted -- see
|
|
772
770
|
* MonitorOwnershipError's own header comment for why claiming first is
|
|
773
771
|
* the only way this refusal can ever be distinguishable from a wedge. */
|
|
774
772
|
claimMonitor(opts: ClaimMonitorOptions): Promise<ClaimMonitorOutcome>;
|
|
@@ -788,7 +786,7 @@ export type OpenBrokerControlOutcome =
|
|
|
788
786
|
|
|
789
787
|
/** The backend-agnostic coordinate set a session which ALREADY holds a
|
|
790
788
|
* broker grant hands to anything that needs to dial the instance that grant
|
|
791
|
-
* names
|
|
789
|
+
* names. Declared here, beside
|
|
792
790
|
* BrokerControlSession and openBrokerControl(), because it is
|
|
793
791
|
* backend-agnostic -- the fork path does not consume it only because
|
|
794
792
|
* forwardToVice() reads activeInstance() from the same module (vice.ts)
|
|
@@ -806,7 +804,7 @@ export interface HeldLease {
|
|
|
806
804
|
port: number;
|
|
807
805
|
targetId: string;
|
|
808
806
|
brokerControl: BrokerControlSession;
|
|
809
|
-
/**
|
|
807
|
+
/** THIS instance's own epoch.json, in the CONSUMER's view of the
|
|
810
808
|
* filesystem (i.e. already containerized -- vice-proxy.ts fills it from
|
|
811
809
|
* activeInstance().epochFile, which adoptGrant() set from the containerized
|
|
812
810
|
* grant). This is the reconnect-identity baseline stock-connect.ts's
|
|
@@ -817,7 +815,7 @@ export interface HeldLease {
|
|
|
817
815
|
* Empty string means genuinely no epoch evidence exists, which is that same
|
|
818
816
|
* unprovable case stated explicitly rather than by omission. */
|
|
819
817
|
epochFile: string;
|
|
820
|
-
/**
|
|
818
|
+
/** The TOP-LEVEL supervisor directory -- the one holding
|
|
821
819
|
* `backend.json`, i.e. the same directory `broker.json` is read from
|
|
822
820
|
* (brokerRootDir()). Deliberately NOT the grant's own per-instance
|
|
823
821
|
* `supervisor_dir` (`<stateDir>/<port>`), which holds epoch.json and would
|
|
@@ -827,7 +825,7 @@ export interface HeldLease {
|
|
|
827
825
|
* backend-detect.mts's own documented degradation for an omitted
|
|
828
826
|
* supervisorDir. */
|
|
829
827
|
supervisorDir: string;
|
|
830
|
-
/**
|
|
828
|
+
/** THIS
|
|
831
829
|
* instance's own text-monitor port, read by text-connect.ts's
|
|
832
830
|
* textConnect() to dial the `-remotemonitor` channel. MANDATORY on a
|
|
833
831
|
* stock grant, ABSENT on a fork grant -- the fork never launches with
|
|
@@ -868,9 +866,9 @@ type RawLineOutcome = { ok: true; line: Record<string, unknown> } | { ok: false;
|
|
|
868
866
|
|
|
869
867
|
/** Never-throw extraction of a `holder` payload from untrusted wire input --
|
|
870
868
|
* absent or malformed input answers `undefined`, never a partially-filled
|
|
871
|
-
* object (
|
|
872
|
-
* this file's own header comment on broker.json reads). `channel`
|
|
873
|
-
*
|
|
869
|
+
* object (this module's own never-throw-on-untrusted-input posture, matching
|
|
870
|
+
* this file's own header comment on broker.json reads). `channel`
|
|
871
|
+
* defaults to `requestedChannel` -- THE channel this request
|
|
874
872
|
* itself named -- when the wire omits it or sends something unrecognised;
|
|
875
873
|
* never fabricated as a plausible value, in the same register the
|
|
876
874
|
* `grantId: "unknown"` fallback one layer up (claimMonitor()'s own) uses. */
|
|
@@ -960,10 +958,10 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
960
958
|
}
|
|
961
959
|
if (line.kind === "error") {
|
|
962
960
|
const code = typeof line.code === "string" ? (line.code as ControlFailureKind) : "internal";
|
|
963
|
-
//
|
|
961
|
+
// Forward `holder` verbatim ONLY for monitor_owned --
|
|
964
962
|
// every other error code carries no such field on the wire, and
|
|
965
963
|
// extractHolder() itself never invents one from absent/malformed
|
|
966
|
-
// input.
|
|
964
|
+
// input. The requested channel comes from
|
|
967
965
|
// THIS payload (the request this response answers), read from
|
|
968
966
|
// the same closure `payload` sendAndAwaitLine() was called
|
|
969
967
|
// with -- an absent/malformed wire `channel` is never fabricated,
|
|
@@ -997,7 +995,7 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
997
995
|
|
|
998
996
|
async function acquire(opts: ControlDeadlineOptions & AcquireProfileOptions = {}): Promise<ControlAcquireResult> {
|
|
999
997
|
const requestId = newRequestId();
|
|
1000
|
-
//
|
|
998
|
+
// Write site TWO of two (see
|
|
1001
999
|
// acquireProfileFragment()'s own comment for why both matter) -- same
|
|
1002
1000
|
// key-omitted-when-absent discipline as acquireOverControlPlane()'s raw
|
|
1003
1001
|
// socket.write above.
|
|
@@ -1060,7 +1058,7 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
1060
1058
|
// in-memory read on the broker side (no launch, no kill involved), so it
|
|
1061
1059
|
// needs no timeout of its own scale; introducing a distinct constant (or
|
|
1062
1060
|
// environment variable) for it would be exactly the kind of new knob
|
|
1063
|
-
//
|
|
1061
|
+
// this module deliberately declines to add.
|
|
1064
1062
|
const raw = await sendAndAwaitLine({ op: "status", token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
|
|
1065
1063
|
if (!raw.ok) return raw;
|
|
1066
1064
|
const line = raw.line;
|
|
@@ -1099,7 +1097,7 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
1099
1097
|
warm_floor: Number(line.warm_floor),
|
|
1100
1098
|
max_instances: Number(line.max_instances),
|
|
1101
1099
|
base_port: Number(line.base_port),
|
|
1102
|
-
//
|
|
1100
|
+
// Narrowed at the boundary, never cast --
|
|
1103
1101
|
// anything other than the one known verdict reads as `null` ("this
|
|
1104
1102
|
// broker did not tell us"), which callers must treat as absent
|
|
1105
1103
|
// evidence rather than agreement.
|
|
@@ -1113,19 +1111,19 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
1113
1111
|
* `sendAndAwaitLine()` path -- the same session, the same token, the same
|
|
1114
1112
|
* newline-delimited JSON discipline every other op uses; no second
|
|
1115
1113
|
* control connection is ever opened, and this function never dials the
|
|
1116
|
-
* binmon port itself, on success OR on failure
|
|
1117
|
-
*
|
|
1118
|
-
* made BEFORE any binmon connect()
|
|
1114
|
+
* binmon port itself, on success OR on failure -- see
|
|
1115
|
+
* MonitorOwnershipError's own header comment for why the claim is
|
|
1116
|
+
* made BEFORE any binmon connect(). `timeout` is reported distinctly
|
|
1119
1117
|
* from `monitor_owned`: a timeout means the broker did not answer, never
|
|
1120
|
-
* that someone else owns the socket. `channel`
|
|
1121
|
-
* to `"binary"` when omitted -- byte-identical to every pre-
|
|
1118
|
+
* that someone else owns the socket. `channel` defaults
|
|
1119
|
+
* to `"binary"` when omitted -- byte-identical to every pre-existing call. */
|
|
1122
1120
|
async function claimMonitor(opts: ClaimMonitorOptions): Promise<ClaimMonitorOutcome> {
|
|
1123
1121
|
const requestId = newRequestId();
|
|
1124
1122
|
const channel: MonitorClaimChannel = opts.channel ?? "binary";
|
|
1125
1123
|
const raw = await sendAndAwaitLine({ op: "monitor_claim", id: requestId, target_id: opts.targetId, channel, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
|
|
1126
1124
|
if (!raw.ok) {
|
|
1127
1125
|
if (raw.kind === "deadline") return { ok: false, reason: "timeout" };
|
|
1128
|
-
//
|
|
1126
|
+
// The `monitor_owned` REASON survives even when the wire's own
|
|
1129
1127
|
// `holder` payload is absent or malformed. This used to be
|
|
1130
1128
|
// `raw.kind === "monitor_owned" && raw.holder`, so a partially-malformed
|
|
1131
1129
|
// refusal collapsed to `{ ok: false, reason: "internal" }` -- stockConnect()
|
|
@@ -1154,7 +1152,7 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
1154
1152
|
* session. Tolerates a broker that has already cleared the record (the
|
|
1155
1153
|
* broker's own onMonitorRelease answers `ok: true` for an already-cleared
|
|
1156
1154
|
* target) -- this function never retries and never opens a second
|
|
1157
|
-
* connection. `channel`
|
|
1155
|
+
* connection. `channel` defaults to `"binary"` when
|
|
1158
1156
|
* omitted. */
|
|
1159
1157
|
async function releaseMonitor(opts: ReleaseMonitorOptions): Promise<ReleaseMonitorOutcome> {
|
|
1160
1158
|
const requestId = newRequestId();
|
|
@@ -1268,5 +1266,5 @@ export function openBrokerControl(dir: string = brokerRootDir(), opts: OpenBroke
|
|
|
1268
1266
|
}
|
|
1269
1267
|
|
|
1270
1268
|
// ---------------------------------------------------------------------------
|
|
1271
|
-
// BROKER-CONTROL-CLIENT REGION END
|
|
1269
|
+
// BROKER-CONTROL-CLIENT REGION END
|
|
1272
1270
|
// ---------------------------------------------------------------------------
|