@henols/vice-mcp 0.2.1 → 0.2.3
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/README.md +4 -3
- package/THIRD-PARTY-NOTICES.md +423 -25
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +3040 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +919 -0
- package/anno-export-asm.ts +2396 -0
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +150 -0
- package/anno-join.ts +480 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/anno-provenance-ledger.ts +472 -0
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +399 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +4002 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +3264 -0
- package/anno-types.ts +1909 -0
- package/backend-detect.mts +124 -312
- package/block-class.ts +201 -0
- package/build.ts +4 -2
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/hostpath.ts +1 -1
- package/incident-record.ts +23 -12
- package/install-resources.ts +30 -14
- package/memmap-lookup.ts +285 -0
- package/package.json +48 -23
- package/prg-image.ts +118 -0
- package/repo-root.ts +107 -8
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +373 -214
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +130 -12
- package/stock-address.ts +6 -6
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +8 -8
- package/stock-connect.ts +10 -6
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +200 -68
- package/stock-execution.ts +5 -3
- package/stock-handler.ts +1 -1
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +17 -12
- package/stock-recycle.ts +83 -2
- package/stock-registers.ts +54 -29
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +13 -13
- package/stock-timing.ts +2 -2
- package/stock-vicii.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/version.ts +1 -1
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +392 -2175
- package/vsf-slice.ts +640 -0
- package/capability-registry.ts +0 -388
- package/r2000-cli.ts +0 -1103
- package/r2000-d64.ts +0 -310
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
package/version.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The ONE authoritative implementation of this project's version-resolution
|
|
2
2
|
// algorithm (D-5). Before this file existed the repo carried FOUR
|
|
3
3
|
// hand-maintained version strings and none of them were true:
|
|
4
|
-
//
|
|
4
|
+
// `src/mcp/vice/package.json` and `installer/package.json` were stale
|
|
5
5
|
// placeholders CI never touched between releases, `.claude-plugin/plugin.json`
|
|
6
6
|
// was bumped by NO automation at all, and `vice-proxy.ts`'s own
|
|
7
7
|
// `PROXY_VERSION` literal -- advertised to every MCP client over
|
package/vice-broker-client.ts
CHANGED
|
@@ -21,16 +21,25 @@
|
|
|
21
21
|
// never-cache-a-negative-result" section.
|
|
22
22
|
//
|
|
23
23
|
// MUST NOT import hostpath.ts: the host-path consumer set is closed to
|
|
24
|
-
// exactly
|
|
25
|
-
// stock-paths.ts, vice-proxy.ts,
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// exactly four production modules (containerpath.ts, install-resources.ts,
|
|
25
|
+
// stock-paths.ts, vice-proxy.ts), pinned by hostpath-consumers.test.ts, and
|
|
26
|
+
// host-path message text stays in vice-proxy.ts, which is already on that
|
|
27
|
+
// list.
|
|
28
28
|
import { readFileSync } from "node:fs";
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
30
|
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
|
+
// Phase 33, plan 33-06 (D-15): TYPE-ONLY, and IMPORTED rather than
|
|
35
|
+
// redeclared. broker-launch.mts is the one definition of the profile shape and
|
|
36
|
+
// the module that turns a profile into argv; a second local shape here is how
|
|
37
|
+
// a client would start requesting a knob the host cannot honour. Type-only, so
|
|
38
|
+
// the container-side bundle never resolves the host-bound module at runtime.
|
|
39
|
+
import type { LaunchProfile } from "./broker-launch.mts";
|
|
40
|
+
// backend-detect.mts is ViceBackend's one home (narrowed to a single literal
|
|
41
|
+
// by FORKRM-01, plan 52-06). Type-only, same discipline as the import above.
|
|
42
|
+
import type { ViceBackend } from "./backend-detect.mts";
|
|
34
43
|
// The module tree's ONE definition of the container-visible host alias
|
|
35
44
|
// (vice.ts:49), carrying its own VICE_MCP_HOST override -- consumed below by
|
|
36
45
|
// resolveControlTarget() rather than a fourth `host.docker.internal` literal
|
|
@@ -44,7 +53,7 @@ import { supervisorDir } from "./repo-root.ts";
|
|
|
44
53
|
// and importing it would pull `hostpath.ts` into this module, which this
|
|
45
54
|
// file's own header (lines 23-26) forbids and which the host-path
|
|
46
55
|
// consumer-set assertion polices.
|
|
47
|
-
import { mcpHost, ViceError } from "./vice.ts";
|
|
56
|
+
import { mcpHost, ViceError } from "./vice-errors.ts";
|
|
48
57
|
|
|
49
58
|
// -------------------------------------------------------------- request ids
|
|
50
59
|
//
|
|
@@ -301,6 +310,24 @@ export interface AcquireGrant {
|
|
|
301
310
|
url: string;
|
|
302
311
|
epoch_file: string;
|
|
303
312
|
supervisor_dir: string;
|
|
313
|
+
/** Plan 41-01 (D-15); made mandatory-in-fact by plan 41-05 (D-16): the
|
|
314
|
+
* broker-allocated port stock's `-remotemonitor` text monitor binds.
|
|
315
|
+
* Absent on a fork grant only -- a stock grant that could not bind a
|
|
316
|
+
* text-monitor port no longer reaches the wire at all: the acquire fails
|
|
317
|
+
* outright (`no_free_text_port`) before any grant is produced. */
|
|
318
|
+
remote_monitor_port?: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Plan 41-01: parses the wire's `remote_monitor_port` into a validated
|
|
322
|
+
* integer in 1..65535, or `undefined` when the key is absent OR the observed
|
|
323
|
+
* value is not a valid port -- never a fabricated 0/null standing in for
|
|
324
|
+
* "no port", and never an unvalidated number handed downstream to a dial.
|
|
325
|
+
* Shared by both AcquireGrant construction sites below so the same
|
|
326
|
+
* validation cannot drift between them. */
|
|
327
|
+
function parseOptionalRemoteMonitorPort(value: unknown): number | undefined {
|
|
328
|
+
if (value === undefined) return undefined;
|
|
329
|
+
const n = Number(value);
|
|
330
|
+
return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : undefined;
|
|
304
331
|
}
|
|
305
332
|
|
|
306
333
|
export interface AcquireOverControlPlaneHandle {
|
|
@@ -327,13 +354,61 @@ export interface AcquireOverControlPlaneHandle {
|
|
|
327
354
|
* client's generic timeout. */
|
|
328
355
|
export const CONTROL_ACQUIRE_TIMEOUT_MS: number = Number(process.env.VICE_BROKER_ACQUIRE_TIMEOUT_MS || 120000);
|
|
329
356
|
|
|
357
|
+
/** Phase 33, plan 33-06 (REPRO-05, D-15): the request-side launch profile.
|
|
358
|
+
* Optional and absent by default at BOTH acquire write sites in this file.
|
|
359
|
+
*
|
|
360
|
+
* ONE RULE, and it is the whole reason this shape is named rather than
|
|
361
|
+
* inlined: the `profile` key must be OMITTED ENTIRELY when no profile was
|
|
362
|
+
* requested, not written as `profile: undefined`. `JSON.stringify` drops an
|
|
363
|
+
* `undefined` value, so both spellings happen to produce the same bytes today
|
|
364
|
+
* -- but an explicit `null` or `{}` would not, and the property this phase
|
|
365
|
+
* needs is that a profile-less acquire's wire line is BYTE-IDENTICAL to the
|
|
366
|
+
* one this client has always written. The spread idiom below is what makes
|
|
367
|
+
* that structural rather than incidental.
|
|
368
|
+
*
|
|
369
|
+
* The SECOND rule, which cost this plan its own dedicated must-have: the
|
|
370
|
+
* profile has to be written at BOTH sites. This file has two independent
|
|
371
|
+
* acquire writers -- acquireOverControlPlane()'s raw `socket.write` below and
|
|
372
|
+
* openBrokerControl()'s `sendAndAwaitLine` further down. A field added to only
|
|
373
|
+
* one of them silently never arrives for callers on the other path, which is
|
|
374
|
+
* the same defect class as a tool argument that is accepted and dropped.
|
|
375
|
+
*
|
|
376
|
+
* CONSUMER STATUS: SUBSTRATE, NOT YET WIRED (33 review WR-08). The profile
|
|
377
|
+
* threads client -> wire -> narrowing -> eligibility -> argv -> record with
|
|
378
|
+
* tests at every hop, but as of phase 33 NO production call site passes one:
|
|
379
|
+
* `acquireOverControlPlane()` and `BrokerControlSession.acquire()` are only
|
|
380
|
+
* ever invoked without `opts.profile`, so `-warp` and `-console` are
|
|
381
|
+
* unreachable in production. That is deliberate -- phase 33 built the chain
|
|
382
|
+
* ahead of phases 34-38 using it -- and is recorded here rather than left for
|
|
383
|
+
* a reader to discover, because a fully-tested chain reads as a live one.
|
|
384
|
+
*
|
|
385
|
+
* Do NOT close this by inventing a call site. Note also that the profile is
|
|
386
|
+
* refused outright on the fork backend (33 review WR-03, broker-control.mts):
|
|
387
|
+
* it maps to stock-only launch flags, so the first real consumer has to be on
|
|
388
|
+
* stock. */
|
|
389
|
+
export interface AcquireProfileOptions {
|
|
390
|
+
profile?: LaunchProfile;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Builds the `profile` fragment of an acquire request line -- the ONE place
|
|
394
|
+
* this client decides whether the key appears at all, so the two write sites
|
|
395
|
+
* cannot drift apart on that decision. Returns an empty object (no key) when
|
|
396
|
+
* no profile was requested. */
|
|
397
|
+
function acquireProfileFragment(profile?: LaunchProfile): { profile?: LaunchProfile } {
|
|
398
|
+
return profile === undefined ? {} : { profile };
|
|
399
|
+
}
|
|
400
|
+
|
|
330
401
|
/** Reads broker.json ONCE for control_host/control_port/control_token,
|
|
331
402
|
* opens ONE TCP connection, sends a single `acquire` request framed as one
|
|
332
403
|
* JSON line, and awaits the grant line against
|
|
333
404
|
* CONTROL_ACQUIRE_TIMEOUT_MS. Rejects (never throws synchronously) on any
|
|
334
405
|
* failure: broker.json absent/unreadable/missing the control fields, a
|
|
335
|
-
* connection error, an `error` response, or a timeout.
|
|
336
|
-
|
|
406
|
+
* connection error, an `error` response, or a timeout.
|
|
407
|
+
*
|
|
408
|
+
* Phase 33, plan 33-06: takes an optional `profile` (see
|
|
409
|
+
* AcquireProfileOptions above). Omitting it writes the exact wire line this
|
|
410
|
+
* function has always written. */
|
|
411
|
+
export function acquireOverControlPlane(dir: string = brokerRootDir(), opts: AcquireProfileOptions = {}): Promise<AcquireOverControlPlaneHandle> {
|
|
337
412
|
return new Promise((resolvePromise, reject) => {
|
|
338
413
|
const broker = readJsonMaybe(brokerJsonPath(dir));
|
|
339
414
|
if (broker === null) {
|
|
@@ -369,7 +444,11 @@ export function acquireOverControlPlane(dir: string = brokerRootDir()): Promise<
|
|
|
369
444
|
|
|
370
445
|
socket.on("connect", () => {
|
|
371
446
|
const requestId = newRequestId();
|
|
372
|
-
|
|
447
|
+
// Phase 33, plan 33-06: write site ONE of two (see
|
|
448
|
+
// acquireProfileFragment()'s own comment) -- the key is absent entirely
|
|
449
|
+
// when no profile was requested, so this line stays byte-identical to
|
|
450
|
+
// what it always was for a profile-less acquire.
|
|
451
|
+
socket.write(`${JSON.stringify({ op: "acquire", id: requestId, token, ...acquireProfileFragment(opts.profile) })}\n`);
|
|
373
452
|
});
|
|
374
453
|
|
|
375
454
|
socket.on("data", (chunk: Buffer) => {
|
|
@@ -400,12 +479,14 @@ export function acquireOverControlPlane(dir: string = brokerRootDir()): Promise<
|
|
|
400
479
|
if (resp.kind === "grant") {
|
|
401
480
|
settled = true;
|
|
402
481
|
clearTimeout(timer);
|
|
482
|
+
const remoteMonitorPort = parseOptionalRemoteMonitorPort(resp.remote_monitor_port);
|
|
403
483
|
const grant: AcquireGrant = {
|
|
404
484
|
id: String(resp.id),
|
|
405
485
|
port: Number(resp.port),
|
|
406
486
|
url: String(resp.url),
|
|
407
487
|
epoch_file: String(resp.epoch_file),
|
|
408
488
|
supervisor_dir: String(resp.supervisor_dir),
|
|
489
|
+
...(remoteMonitorPort === undefined ? {} : { remote_monitor_port: remoteMonitorPort }),
|
|
409
490
|
};
|
|
410
491
|
resolvePromise({
|
|
411
492
|
grant,
|
|
@@ -550,37 +631,56 @@ interface ControlHostStateFields {
|
|
|
550
631
|
warm_floor: number;
|
|
551
632
|
max_instances: number;
|
|
552
633
|
base_port: number;
|
|
553
|
-
/**
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
backend:
|
|
634
|
+
/** FORKRM-01 (plan 52-06): narrowed from `"fork" | "stock" | null` to
|
|
635
|
+
* `ViceBackend | null` -- `null` when the broker predates this field or
|
|
636
|
+
* sent something unrecognised: absent evidence, kept distinct from a
|
|
637
|
+
* definite value. text-tools.ts's own broker-identity cross-check (out of
|
|
638
|
+
* this plan's scope) still reads this field. */
|
|
639
|
+
backend: ViceBackend | null;
|
|
559
640
|
}
|
|
560
641
|
|
|
561
642
|
export type ControlHostStateResult =
|
|
562
643
|
| { ok: true; hostState: ControlHostStateFields }
|
|
563
644
|
| { ok: false; kind: ControlFailureKind; message: string };
|
|
564
645
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
// MonitorClaimChannel (plan 41-03, D-14): the two-value channel contract,
|
|
648
|
+
// declared HERE as a local literal union rather than imported from
|
|
649
|
+
// broker-state.mts -- that module is host-bound and compiled into
|
|
650
|
+
// resources/*.mjs, and this file is the container-side half. The shared
|
|
651
|
+
// thing between the declarations is the CONTRACT ("binary" | "text"), not
|
|
652
|
+
// the declaration itself -- channel-lock.ts's own MonitorChannel and
|
|
653
|
+
// broker-state.mts's own MonitorChannel each declare it separately for the
|
|
654
|
+
// same reason.
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
export type MonitorClaimChannel = "binary" | "text";
|
|
657
|
+
|
|
658
|
+
/** Plan 05 (BROK-02/PROTO-08, D-13; gains `channel` in plan 41-03, D-14): the
|
|
659
|
+
* current monitor-socket holder's own identity, named in a `monitor_owned`
|
|
660
|
+
* refusal -- field-for-field the same shape the broker's own MonitorHolder
|
|
661
|
+
* carries (broker-control.mts), minus nothing (pid included, matching
|
|
662
|
+
* GrantRecord's own convention this whole mechanism mirrors). */
|
|
570
663
|
export interface MonitorClaimHolder {
|
|
571
664
|
grantId: string;
|
|
572
665
|
claimedAt: number;
|
|
573
666
|
pid: number | null;
|
|
667
|
+
channel: MonitorClaimChannel;
|
|
574
668
|
}
|
|
575
669
|
|
|
576
670
|
export interface ClaimMonitorOptions {
|
|
577
671
|
targetId: string;
|
|
578
672
|
timeoutMs?: number;
|
|
673
|
+
/** Plan 41-03 (D-14): which monitor socket to claim. Omitted is
|
|
674
|
+
* byte-identical to `"binary"` -- every pre-41-03 call site (and every
|
|
675
|
+
* broker that predates this field) keeps working unchanged. */
|
|
676
|
+
channel?: MonitorClaimChannel;
|
|
579
677
|
}
|
|
580
678
|
|
|
581
679
|
export interface ReleaseMonitorOptions {
|
|
582
680
|
targetId: string;
|
|
583
681
|
timeoutMs?: number;
|
|
682
|
+
/** Plan 41-03 (D-14): same default-to-binary posture as ClaimMonitorOptions.channel. */
|
|
683
|
+
channel?: MonitorClaimChannel;
|
|
584
684
|
}
|
|
585
685
|
|
|
586
686
|
/** Discriminated claim outcome (plan 05): `monitor_owned` is kept STRICTLY
|
|
@@ -607,6 +707,9 @@ export interface MonitorOwnershipErrorOptions {
|
|
|
607
707
|
holderGrantId?: string;
|
|
608
708
|
holderClaimedAt?: number;
|
|
609
709
|
port?: number;
|
|
710
|
+
/** Plan 41-03 (D-14): which socket is contended -- so a handshake failure
|
|
711
|
+
* can say which channel was refused without re-parsing the message. */
|
|
712
|
+
channel?: MonitorClaimChannel;
|
|
610
713
|
}
|
|
611
714
|
|
|
612
715
|
/** Thrown (by a caller that prefers to raise rather than branch on
|
|
@@ -628,13 +731,15 @@ export class MonitorOwnershipError extends ViceError {
|
|
|
628
731
|
holderGrantId?: string;
|
|
629
732
|
holderClaimedAt?: number;
|
|
630
733
|
port?: number;
|
|
734
|
+
channel?: MonitorClaimChannel;
|
|
631
735
|
|
|
632
|
-
constructor(message: string, { holderGrantId, holderClaimedAt, port }: MonitorOwnershipErrorOptions = {}) {
|
|
736
|
+
constructor(message: string, { holderGrantId, holderClaimedAt, port, channel }: MonitorOwnershipErrorOptions = {}) {
|
|
633
737
|
super(message);
|
|
634
738
|
this.name = "MonitorOwnershipError";
|
|
635
739
|
this.holderGrantId = holderGrantId;
|
|
636
740
|
this.holderClaimedAt = holderClaimedAt;
|
|
637
741
|
this.port = port;
|
|
742
|
+
this.channel = channel;
|
|
638
743
|
}
|
|
639
744
|
}
|
|
640
745
|
|
|
@@ -654,7 +759,10 @@ export interface ControlDeadlineOptions {
|
|
|
654
759
|
* exactly one request line and resolves against its own deadline; none of
|
|
655
760
|
* them ever reject. */
|
|
656
761
|
export interface BrokerControlSession {
|
|
657
|
-
|
|
762
|
+
/** Phase 33, plan 33-06 (REPRO-05, D-15): additionally takes an optional
|
|
763
|
+
* `profile` -- see AcquireProfileOptions. Omitting it is byte-identical to
|
|
764
|
+
* every pre-33-06 call. */
|
|
765
|
+
acquire(opts?: ControlDeadlineOptions & AcquireProfileOptions): Promise<ControlAcquireResult>;
|
|
658
766
|
release(): Promise<ControlReleaseResult>;
|
|
659
767
|
recycle(targetId: string, opts?: ControlDeadlineOptions): Promise<ControlRecycleResult>;
|
|
660
768
|
status(opts?: ControlDeadlineOptions): Promise<ControlStatusResult>;
|
|
@@ -719,6 +827,21 @@ export interface HeldLease {
|
|
|
719
827
|
* backend-detect.mts's own documented degradation for an omitted
|
|
720
828
|
* supervisorDir. */
|
|
721
829
|
supervisorDir: string;
|
|
830
|
+
/** Plan 41-01 (D-15); made mandatory-in-fact by plan 41-05 (D-16): THIS
|
|
831
|
+
* instance's own text-monitor port, read by text-connect.ts's
|
|
832
|
+
* textConnect() to dial the `-remotemonitor` channel. MANDATORY on a
|
|
833
|
+
* stock grant, ABSENT on a fork grant -- the fork never launches with
|
|
834
|
+
* `-remotemonitor` and advertises no text tools. Its absence on a stock
|
|
835
|
+
* lease is a real defect, not a tolerated state (mirrors epochFile's own
|
|
836
|
+
* "NOT optional" discipline above): the mechanism that makes this true is
|
|
837
|
+
* broker-launch.mts's acquirePortAndLaunch(), which now FAILS THE WHOLE
|
|
838
|
+
* ACQUIRE when the text-port allocation fails (`no_free_text_port`)
|
|
839
|
+
* rather than degrading to a portless launch -- there is no longer a code
|
|
840
|
+
* path that produces a stock grant, and therefore a HeldLease, without
|
|
841
|
+
* this field. Its optionality here is a transitional TypeScript
|
|
842
|
+
* convenience only (the fork case is real), never a semantic "sometimes
|
|
843
|
+
* missing on stock". */
|
|
844
|
+
remoteMonitorPort?: number;
|
|
722
845
|
}
|
|
723
846
|
|
|
724
847
|
/** One in-flight request's settlement callback -- pushed onto the session's
|
|
@@ -746,12 +869,17 @@ type RawLineOutcome = { ok: true; line: Record<string, unknown> } | { ok: false;
|
|
|
746
869
|
/** Never-throw extraction of a `holder` payload from untrusted wire input --
|
|
747
870
|
* absent or malformed input answers `undefined`, never a partially-filled
|
|
748
871
|
* object (plan 05's own never-throw-on-untrusted-input posture, matching
|
|
749
|
-
* this file's own header comment on broker.json reads).
|
|
750
|
-
|
|
872
|
+
* this file's own header comment on broker.json reads). `channel` (plan
|
|
873
|
+
* 41-03, D-14) defaults to `requestedChannel` -- THE channel this request
|
|
874
|
+
* itself named -- when the wire omits it or sends something unrecognised;
|
|
875
|
+
* never fabricated as a plausible value, in the same register the
|
|
876
|
+
* `grantId: "unknown"` fallback one layer up (claimMonitor()'s own) uses. */
|
|
877
|
+
function extractHolder(raw: unknown, requestedChannel: MonitorClaimChannel): MonitorClaimHolder | undefined {
|
|
751
878
|
if (typeof raw !== "object" || raw === null) return undefined;
|
|
752
879
|
const h = raw as Record<string, unknown>;
|
|
753
880
|
if (typeof h.grantId !== "string" || typeof h.claimedAt !== "number") return undefined;
|
|
754
|
-
|
|
881
|
+
const channel = h.channel === "text" || h.channel === "binary" ? h.channel : requestedChannel;
|
|
882
|
+
return { grantId: h.grantId, claimedAt: h.claimedAt, pid: typeof h.pid === "number" ? h.pid : null, channel };
|
|
755
883
|
}
|
|
756
884
|
|
|
757
885
|
/** Builds the session object wrapping an already-CONNECTED socket. Wires the
|
|
@@ -835,8 +963,13 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
835
963
|
// Plan 05: forward `holder` verbatim ONLY for monitor_owned --
|
|
836
964
|
// every other error code carries no such field on the wire, and
|
|
837
965
|
// extractHolder() itself never invents one from absent/malformed
|
|
838
|
-
// input.
|
|
839
|
-
|
|
966
|
+
// input. Plan 41-03 (D-14): the requested channel comes from
|
|
967
|
+
// THIS payload (the request this response answers), read from
|
|
968
|
+
// the same closure `payload` sendAndAwaitLine() was called
|
|
969
|
+
// with -- an absent/malformed wire `channel` is never fabricated,
|
|
970
|
+
// it defaults to the channel this specific request itself named.
|
|
971
|
+
const requestedChannel: MonitorClaimChannel = payload.channel === "text" ? "text" : "binary";
|
|
972
|
+
const holder = code === "monitor_owned" ? extractHolder(line.holder, requestedChannel) : undefined;
|
|
840
973
|
resolvePromise({
|
|
841
974
|
ok: false,
|
|
842
975
|
kind: code,
|
|
@@ -862,20 +995,29 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
862
995
|
});
|
|
863
996
|
}
|
|
864
997
|
|
|
865
|
-
async function acquire(opts: ControlDeadlineOptions = {}): Promise<ControlAcquireResult> {
|
|
998
|
+
async function acquire(opts: ControlDeadlineOptions & AcquireProfileOptions = {}): Promise<ControlAcquireResult> {
|
|
866
999
|
const requestId = newRequestId();
|
|
867
|
-
|
|
1000
|
+
// Phase 33, plan 33-06: write site TWO of two (see
|
|
1001
|
+
// acquireProfileFragment()'s own comment for why both matter) -- same
|
|
1002
|
+
// key-omitted-when-absent discipline as acquireOverControlPlane()'s raw
|
|
1003
|
+
// socket.write above.
|
|
1004
|
+
const raw = await sendAndAwaitLine(
|
|
1005
|
+
{ op: "acquire", id: requestId, token, ...acquireProfileFragment(opts.profile) },
|
|
1006
|
+
opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS,
|
|
1007
|
+
);
|
|
868
1008
|
if (!raw.ok) return raw;
|
|
869
1009
|
const line = raw.line;
|
|
870
1010
|
if (line.kind !== "grant") {
|
|
871
1011
|
return { ok: false, kind: "protocol", message: `openBrokerControl: acquire got unexpected response kind ${String(line.kind)}` };
|
|
872
1012
|
}
|
|
1013
|
+
const remoteMonitorPort = parseOptionalRemoteMonitorPort(line.remote_monitor_port);
|
|
873
1014
|
const grant: AcquireGrant = {
|
|
874
1015
|
id: String(line.id),
|
|
875
1016
|
port: Number(line.port),
|
|
876
1017
|
url: String(line.url),
|
|
877
1018
|
epoch_file: String(line.epoch_file),
|
|
878
1019
|
supervisor_dir: String(line.supervisor_dir),
|
|
1020
|
+
...(remoteMonitorPort === undefined ? {} : { remote_monitor_port: remoteMonitorPort }),
|
|
879
1021
|
};
|
|
880
1022
|
return { ok: true, grant };
|
|
881
1023
|
}
|
|
@@ -957,16 +1099,17 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
957
1099
|
warm_floor: Number(line.warm_floor),
|
|
958
1100
|
max_instances: Number(line.max_instances),
|
|
959
1101
|
base_port: Number(line.base_port),
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
// which callers must treat as absent
|
|
963
|
-
|
|
1102
|
+
// FORKRM-01 (plan 52-06): narrowed at the boundary, never cast --
|
|
1103
|
+
// anything other than the one known verdict reads as `null` ("this
|
|
1104
|
+
// broker did not tell us"), which callers must treat as absent
|
|
1105
|
+
// evidence rather than agreement.
|
|
1106
|
+
backend: line.backend === "stock" ? line.backend : null,
|
|
964
1107
|
},
|
|
965
1108
|
};
|
|
966
1109
|
}
|
|
967
1110
|
|
|
968
1111
|
/** Claims exclusive ownership of `opts.targetId`'s monitor socket, sending
|
|
969
|
-
* `{ op: "monitor_claim", id, target_id, token }` through the SAME
|
|
1112
|
+
* `{ op: "monitor_claim", id, target_id, channel, token }` through the SAME
|
|
970
1113
|
* `sendAndAwaitLine()` path -- the same session, the same token, the same
|
|
971
1114
|
* newline-delimited JSON discipline every other op uses; no second
|
|
972
1115
|
* control connection is ever opened, and this function never dials the
|
|
@@ -974,10 +1117,12 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
974
1117
|
* -- see MonitorOwnershipError's own header comment for why the claim is
|
|
975
1118
|
* made BEFORE any binmon connect()). `timeout` is reported distinctly
|
|
976
1119
|
* from `monitor_owned`: a timeout means the broker did not answer, never
|
|
977
|
-
* that someone else owns the socket.
|
|
1120
|
+
* that someone else owns the socket. `channel` (plan 41-03, D-14) defaults
|
|
1121
|
+
* to `"binary"` when omitted -- byte-identical to every pre-41-03 call. */
|
|
978
1122
|
async function claimMonitor(opts: ClaimMonitorOptions): Promise<ClaimMonitorOutcome> {
|
|
979
1123
|
const requestId = newRequestId();
|
|
980
|
-
const
|
|
1124
|
+
const channel: MonitorClaimChannel = opts.channel ?? "binary";
|
|
1125
|
+
const raw = await sendAndAwaitLine({ op: "monitor_claim", id: requestId, target_id: opts.targetId, channel, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
|
|
981
1126
|
if (!raw.ok) {
|
|
982
1127
|
if (raw.kind === "deadline") return { ok: false, reason: "timeout" };
|
|
983
1128
|
// WR-08: the `monitor_owned` REASON survives even when the wire's own
|
|
@@ -989,11 +1134,11 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
989
1134
|
// requires (and MonitorOwnershipError exists to preserve) was lost. The
|
|
990
1135
|
// broker has told us WHICH state this is; not being able to name the
|
|
991
1136
|
// holder does not make it a different state. Holder fields default to
|
|
992
|
-
// "unknown"/0/null so the wording still reads as an ownership
|
|
993
|
-
// rather than an emulator fault -- never fabricated as a
|
|
994
|
-
// id, which would be worse than admitting it is unknown.
|
|
1137
|
+
// "unknown"/0/null/`channel` so the wording still reads as an ownership
|
|
1138
|
+
// conflict rather than an emulator fault -- never fabricated as a
|
|
1139
|
+
// plausible grant id, which would be worse than admitting it is unknown.
|
|
995
1140
|
if (raw.kind === "monitor_owned") {
|
|
996
|
-
return { ok: false, reason: "monitor_owned", holder: raw.holder ?? { grantId: "unknown", claimedAt: 0, pid: null } };
|
|
1141
|
+
return { ok: false, reason: "monitor_owned", holder: raw.holder ?? { grantId: "unknown", claimedAt: 0, pid: null, channel } };
|
|
997
1142
|
}
|
|
998
1143
|
if (raw.kind === "unauthorized" || raw.kind === "bad_request" || raw.kind === "denied") return { ok: false, reason: raw.kind };
|
|
999
1144
|
return { ok: false, reason: "internal" };
|
|
@@ -1005,14 +1150,16 @@ function createSession(socket: Socket, token: string): BrokerControlSession {
|
|
|
1005
1150
|
}
|
|
1006
1151
|
|
|
1007
1152
|
/** Releases a previously claimed monitor socket, sending
|
|
1008
|
-
* `{ op: "monitor_release", id, target_id, token }` over the SAME
|
|
1153
|
+
* `{ op: "monitor_release", id, target_id, channel, token }` over the SAME
|
|
1009
1154
|
* session. Tolerates a broker that has already cleared the record (the
|
|
1010
1155
|
* broker's own onMonitorRelease answers `ok: true` for an already-cleared
|
|
1011
1156
|
* target) -- this function never retries and never opens a second
|
|
1012
|
-
* connection.
|
|
1157
|
+
* connection. `channel` (plan 41-03, D-14) defaults to `"binary"` when
|
|
1158
|
+
* omitted. */
|
|
1013
1159
|
async function releaseMonitor(opts: ReleaseMonitorOptions): Promise<ReleaseMonitorOutcome> {
|
|
1014
1160
|
const requestId = newRequestId();
|
|
1015
|
-
const
|
|
1161
|
+
const channel: MonitorClaimChannel = opts.channel ?? "binary";
|
|
1162
|
+
const raw = await sendAndAwaitLine({ op: "monitor_release", id: requestId, target_id: opts.targetId, channel, token }, opts.timeoutMs ?? ACQUIRE_TIMEOUT_MS);
|
|
1016
1163
|
if (!raw.ok) {
|
|
1017
1164
|
if (raw.kind === "deadline") return { ok: false, reason: "timeout" };
|
|
1018
1165
|
if (raw.kind === "unauthorized" || raw.kind === "bad_request" || raw.kind === "denied") return { ok: false, reason: raw.kind };
|