ever-works-node 0.2.5 → 0.2.6

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.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/cli.js +3048 -181
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -3509,10 +3509,10 @@ var {
3509
3509
  // src/cli/program.ts
3510
3510
  var import_node_path7 = require("node:path");
3511
3511
 
3512
- // ../../node_modules/.pnpm/@ever-co+connect-contracts@1.0.0-rc.2/node_modules/@ever-co/connect-contracts/dist/index.js
3512
+ // ../../node_modules/.pnpm/@ever-co+connect-contracts@1.0.0-rc.7/node_modules/@ever-co/connect-contracts/dist/index.js
3513
3513
  var CONSTANTS = {
3514
3514
  "$comment": "Wire constants of the Ever Platform modules. tools/split-integrations.mjs refreshes the fields read from the contract (contracts_version, code patterns, install_sources, products, stats_headers, feed_event_types, integration_keys, root_keys); every other value is a default a product may override by its own configuration.",
3515
- "contracts_version": "1.0.0-rc.2",
3515
+ "contracts_version": "1.0.0-rc.7",
3516
3516
  "connect_code_pattern": "^EVC(-[0-9A-HJKMNP-TV-Za-hj-kmnp-tv-z]{4}){3}$",
3517
3517
  "link_code_pattern": "^EVL(-[0-9A-HJKMNP-TV-Za-hj-kmnp-tv-z]{4}){3}$",
3518
3518
  "install_sources": "^(cloud|self-hosted|ever\\.sh|works_app|desktop|partner:[a-z0-9-]{2,32})$",
@@ -3660,6 +3660,15 @@ var CONSTANTS = {
3660
3660
  "x": "Ync-MW21JL72GOiS-RVrlvS3gyFH5Krh5f6I66jw1Qc",
3661
3661
  "use": "sig",
3662
3662
  "alg": "EdDSA"
3663
+ },
3664
+ {
3665
+ "kid": "ever-202610-025b",
3666
+ "iss": "https://api.ever.co",
3667
+ "kty": "OKP",
3668
+ "crv": "Ed25519",
3669
+ "x": "AwXJEZdyiK3QEhUnNB-0fIsN9PPLgbF6O3tyZT3moDc",
3670
+ "use": "sig",
3671
+ "alg": "EdDSA"
3663
3672
  }
3664
3673
  ],
3665
3674
  "root_keys_file_env": "EVER_PLATFORM_ROOT_KEYS_FILE",
@@ -3727,6 +3736,7 @@ var PROBLEM_CODES = [
3727
3736
  "already_connected",
3728
3737
  "already_linked",
3729
3738
  "already_claimed",
3739
+ "claim_locked",
3730
3740
  "invalid_client",
3731
3741
  "public_jwk_invalid",
3732
3742
  "identifier_claimed",
@@ -3752,6 +3762,11 @@ var PROBLEM_CODES = [
3752
3762
  "database_absent",
3753
3763
  "database_unreachable",
3754
3764
  "topic_private",
3765
+ "realtime_disabled",
3766
+ "topic_forbidden",
3767
+ "topic_invalid",
3768
+ "realtime_busy",
3769
+ "too_many_streams",
3755
3770
  "keys_unavailable",
3756
3771
  "internal_error"
3757
3772
  ];
@@ -4967,6 +4982,36 @@ var SKILL_READINESS_STATES = [
4967
4982
  ];
4968
4983
  var SKILL_CARD_STATES = [...SKILL_READINESS_STATES, "disabled", "needs_review"];
4969
4984
  var SKILL_READINESS_FILTERS = [...SKILL_CARD_STATES, "attention"];
4985
+ var FLEET_CONTROL_POLICY_DEFAULTS = Object.freeze({
4986
+ view: "org-members",
4987
+ sessions: "owner",
4988
+ desktop: "owner",
4989
+ logs: "owner",
4990
+ services: "owner",
4991
+ docker: "owner",
4992
+ processes: "owner",
4993
+ ports: "owner",
4994
+ files: "owner",
4995
+ commands: "owner",
4996
+ schedules: "owner",
4997
+ checkouts: "owner",
4998
+ machines: "owner"
4999
+ });
5000
+ var FLEET_ACTION_FAMILY_HINT_LABELS = Object.freeze({
5001
+ view: "view",
5002
+ sessions: "sessions",
5003
+ desktop: "desktop",
5004
+ logs: "logs",
5005
+ services: "services",
5006
+ docker: "Docker",
5007
+ processes: "processes",
5008
+ ports: "ports",
5009
+ files: "files",
5010
+ commands: "commands",
5011
+ schedules: "schedules",
5012
+ checkouts: "checkouts",
5013
+ machines: "lifecycle"
5014
+ });
4970
5015
  var FLEET_AGENT_CREDENTIAL_FAMILIES = [
4971
5016
  {
4972
5017
  cli: "claude-code",
@@ -5554,6 +5599,581 @@ function truncateToUtf8Bytes(value, maxBytes) {
5554
5599
  return { value: decoded.replace(/\uFFFD+$/, "").trimEnd(), truncated: true };
5555
5600
  }
5556
5601
  __name(truncateToUtf8Bytes, "truncateToUtf8Bytes");
5602
+ var FLEET_LINK_VERSION = 1;
5603
+ var FLEET_LINK_WS_PATH = "/ws/fleet/link";
5604
+ var FLEET_LINK_MAX_FRAME_BYTES = 16 * 1024 * 1024;
5605
+ var FLEET_LINK_FRAME_HEADER_BYTES = 9;
5606
+ var FLEET_LINK_MAX_PAYLOAD_BYTES = FLEET_LINK_MAX_FRAME_BYTES - FLEET_LINK_FRAME_HEADER_BYTES;
5607
+ var FLEET_LINK_MAX_JSON_BYTES = 1024 * 1024;
5608
+ var FLEET_LINK_MAX_CHANNEL_NUMBER = 4294967295;
5609
+ var FLEET_LINK_MAX_CHANNELS = 64;
5610
+ var FLEET_LINK_PING_INTERVAL_MS = 15e3;
5611
+ var FLEET_LINK_SILENCE_MS = 45e3;
5612
+ var FLEET_LINK_BACKOFF_BASE_MS = 1e3;
5613
+ var FLEET_LINK_BACKOFF_MAX_MS = 3e4;
5614
+ var FLEET_LINK_REFUSED_RETRY_MS = 5 * 6e4;
5615
+ var FLEET_LINK_HELLO_BLOCK_MS = 6e4;
5616
+ var FLEET_LINK_AUTH_TIMEOUT_MS = 5e3;
5617
+ var FLEET_LINK_INLINE_REPLY_MS = 2e3;
5618
+ var FLEET_LINK_RESUME_WINDOW_MS = 12e4;
5619
+ var FLEET_OPERATION_RETENTION_MS = 60 * 6e4;
5620
+ var FLEET_LINK_DEFAULT_LIMITS = {
5621
+ maxFrameBytes: FLEET_LINK_MAX_FRAME_BYTES,
5622
+ maxChannels: FLEET_LINK_MAX_CHANNELS,
5623
+ pingIntervalMs: FLEET_LINK_PING_INTERVAL_MS,
5624
+ silenceMs: FLEET_LINK_SILENCE_MS,
5625
+ inlineReplyMs: FLEET_LINK_INLINE_REPLY_MS
5626
+ };
5627
+ var FLEET_LINK_CLOSE_CODES = {
5628
+ /** Normal close (a node shutting down, a channel-less link released). */
5629
+ normal: 1e3,
5630
+ /** The API process is shutting down. */
5631
+ shutdown: 1001,
5632
+ /** Undifferentiated credential refusal. */
5633
+ auth: 4001,
5634
+ /** 45 s without a frame. */
5635
+ silence: 4002,
5636
+ /** This deployment does not accept node links (`FLEET_NODE_LINK_ENABLED` off). */
5637
+ disabled: 4003,
5638
+ /** Too many hellos (CONTRACTS U9). */
5639
+ rateLimited: 4008,
5640
+ /** The other side broke the protocol (bad first frame, a node-opened channel, …). */
5641
+ protocol: 4009,
5642
+ /** A newer link from the same node replaced this one. */
5643
+ replaced: 4010,
5644
+ /** The node presented no consent that allows a link (work, attend, manage all off). */
5645
+ noConsent: 4012
5646
+ };
5647
+ var FLEET_LINK_FRAME_KINDS = {
5648
+ /** Raw bytes: terminal output/input, log lines, file chunks. */
5649
+ DATA: 1,
5650
+ /** `[cols u16][rows u16]`. */
5651
+ RESIZE: 2,
5652
+ /** `[code i32]`. */
5653
+ EXIT: 3,
5654
+ /** UTF-8 JSON control message. */
5655
+ JSON: 10,
5656
+ /** UTF-8 reason code. */
5657
+ CLOSE: 11,
5658
+ /** A full picture (the existing computer-frame encoding). */
5659
+ FRAME: 20,
5660
+ /** A pointer / key / text event (the existing computer input encoding). */
5661
+ INPUT: 21,
5662
+ /** Changed tiles `[w][h][count]{x,y,w,h,len,jpeg}`. */
5663
+ TILES: 23,
5664
+ /** `[bytes u32]` credit. */
5665
+ ACK: 30,
5666
+ /** End of a stream. */
5667
+ END: 31,
5668
+ /** UTF-8 text ≤ 256 KiB, pasted into a desktop. */
5669
+ CLIP: 40
5670
+ };
5671
+ var FRAME_KIND_VALUES = new Set(Object.values(FLEET_LINK_FRAME_KINDS));
5672
+ function isFleetLinkFrameKind(value) {
5673
+ return typeof value === "number" && FRAME_KIND_VALUES.has(value);
5674
+ }
5675
+ __name(isFleetLinkFrameKind, "isFleetLinkFrameKind");
5676
+ var FLEET_LINK_MAX_CLIP_BYTES = 256 * 1024;
5677
+ var FLEET_LINK_CREDIT_WINDOW_BYTES = 8 * 1024 * 1024;
5678
+ var FLEET_LINK_CREDIT_FAMILIES = ["files", "logs"];
5679
+ var FLEET_CONSENT_FLAGS = {
5680
+ work: "--work",
5681
+ attend: "--attend",
5682
+ manage: "--manage",
5683
+ usageScan: "--usage-scan"
5684
+ };
5685
+ var FLEET_NO_CONSENTS = Object.freeze({
5686
+ work: false,
5687
+ attend: false,
5688
+ manage: false,
5689
+ usageScan: false
5690
+ });
5691
+ function normalizeFleetConsents(value) {
5692
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
5693
+ const record = value;
5694
+ return {
5695
+ work: record.work === true,
5696
+ attend: record.attend === true,
5697
+ manage: record.manage === true,
5698
+ usageScan: record.usageScan === true
5699
+ };
5700
+ }
5701
+ __name(normalizeFleetConsents, "normalizeFleetConsents");
5702
+ function isFleetLinkEligible(consents) {
5703
+ return Boolean(consents && (consents.work || consents.attend || consents.manage));
5704
+ }
5705
+ __name(isFleetLinkEligible, "isFleetLinkEligible");
5706
+ var FLEET_LINK_FAMILIES = [
5707
+ "session",
5708
+ "desktop",
5709
+ "logs",
5710
+ "machine",
5711
+ "files",
5712
+ "command",
5713
+ "schedule",
5714
+ "tunnel",
5715
+ "clip"
5716
+ ];
5717
+ function isFleetLinkFamily(value) {
5718
+ return typeof value === "string" && FLEET_LINK_FAMILIES.includes(value);
5719
+ }
5720
+ __name(isFleetLinkFamily, "isFleetLinkFamily");
5721
+ var FLEET_LINK_FAMILY_CONSENT = {
5722
+ session: "work-or-manage",
5723
+ desktop: "attend",
5724
+ logs: "manage",
5725
+ machine: "manage",
5726
+ files: "manage",
5727
+ command: "manage",
5728
+ schedule: "manage",
5729
+ tunnel: "manage",
5730
+ clip: "attend"
5731
+ };
5732
+ var FLEET_LINK_VERB_CONSENT_OVERRIDES = {
5733
+ /** A read allowed on any open link. */
5734
+ "health.now": "link"
5735
+ };
5736
+ function fleetLinkVerbConsent(verb) {
5737
+ return Object.prototype.hasOwnProperty.call(FLEET_LINK_VERB_CONSENT_OVERRIDES, verb) ? FLEET_LINK_VERB_CONSENT_OVERRIDES[verb] : FLEET_LINK_FAMILY_CONSENT.machine;
5738
+ }
5739
+ __name(fleetLinkVerbConsent, "fleetLinkVerbConsent");
5740
+ function fleetLinkChannelOpenConsent(family) {
5741
+ if (family === "machine" && Object.keys(FLEET_LINK_VERB_CONSENT_OVERRIDES).length > 0) return "link";
5742
+ return FLEET_LINK_FAMILY_CONSENT[family];
5743
+ }
5744
+ __name(fleetLinkChannelOpenConsent, "fleetLinkChannelOpenConsent");
5745
+ function fleetConsentSatisfies(consents, requirement) {
5746
+ if (!consents) return false;
5747
+ switch (requirement) {
5748
+ case "link":
5749
+ return isFleetLinkEligible(consents);
5750
+ case "work":
5751
+ return consents.work;
5752
+ case "attend":
5753
+ return consents.attend;
5754
+ case "manage":
5755
+ return consents.manage;
5756
+ case "work-or-manage":
5757
+ return consents.work || consents.manage;
5758
+ default:
5759
+ return false;
5760
+ }
5761
+ }
5762
+ __name(fleetConsentSatisfies, "fleetConsentSatisfies");
5763
+ function fleetConsentFlagsFor(requirement) {
5764
+ switch (requirement) {
5765
+ case "work":
5766
+ return [FLEET_CONSENT_FLAGS.work];
5767
+ case "attend":
5768
+ return [FLEET_CONSENT_FLAGS.attend];
5769
+ case "manage":
5770
+ return [FLEET_CONSENT_FLAGS.manage];
5771
+ case "work-or-manage":
5772
+ return [FLEET_CONSENT_FLAGS.work, FLEET_CONSENT_FLAGS.manage];
5773
+ case "link":
5774
+ default:
5775
+ return [FLEET_CONSENT_FLAGS.work, FLEET_CONSENT_FLAGS.attend, FLEET_CONSENT_FLAGS.manage];
5776
+ }
5777
+ }
5778
+ __name(fleetConsentFlagsFor, "fleetConsentFlagsFor");
5779
+ function fleetConsentMissingHint(requirement, nodeName) {
5780
+ const flags = fleetConsentFlagsFor(requirement);
5781
+ const joined = flags.length === 1 ? flags[0] : `${flags.slice(0, -1).join(", ")} or ${flags[flags.length - 1]}`;
5782
+ return `start the node with ${joined} on ${nodeName}`;
5783
+ }
5784
+ __name(fleetConsentMissingHint, "fleetConsentMissingHint");
5785
+ var VERB_PATTERN = /^[a-z][a-zA-Z0-9]{0,31}(?:\.[a-z][a-zA-Z0-9]{0,31}){0,3}$/;
5786
+ function isFleetLinkVerb(value) {
5787
+ return typeof value === "string" && VERB_PATTERN.test(value);
5788
+ }
5789
+ __name(isFleetLinkVerb, "isFleetLinkVerb");
5790
+ var FLEET_LINK_ERROR_CODES = [
5791
+ "not_found",
5792
+ "invalid",
5793
+ "unreachable",
5794
+ "consent_missing",
5795
+ "permission_denied",
5796
+ "confirmation_required",
5797
+ "root_required",
5798
+ "kill_switch",
5799
+ "limit_exceeded",
5800
+ "unsupported_on_node",
5801
+ "busy",
5802
+ "operation_failed",
5803
+ "internal",
5804
+ /** A channel the reconnect could not resume (CONTRACTS §3.1). */
5805
+ "resume_failed"
5806
+ ];
5807
+ function isFleetLinkErrorCode(value) {
5808
+ return typeof value === "string" && FLEET_LINK_ERROR_CODES.includes(value);
5809
+ }
5810
+ __name(isFleetLinkErrorCode, "isFleetLinkErrorCode");
5811
+ var FLEET_LINK_MAX_HINT_LENGTH = 500;
5812
+ var FLEET_LINK_CONTROL_TYPES = [
5813
+ "link.hello",
5814
+ "link.ready",
5815
+ "link.ping",
5816
+ "link.pong",
5817
+ "link.pause",
5818
+ "link.resume",
5819
+ "link.drain",
5820
+ "channel.open",
5821
+ "channel.ready",
5822
+ "channel.refused",
5823
+ "channel.close",
5824
+ "operation.progress",
5825
+ "operation.done",
5826
+ "operation.cancel"
5827
+ ];
5828
+ var ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
5829
+ function isFleetLinkId(value) {
5830
+ return typeof value === "string" && ID_PATTERN.test(value);
5831
+ }
5832
+ __name(isFleetLinkId, "isFleetLinkId");
5833
+ var FLEET_LINK_MAX_JSON_DEPTH = 32;
5834
+ var MAX_TOKEN_LENGTH = 4096;
5835
+ var MAX_TAGS = 64;
5836
+ var MAX_TAG_LENGTH = 128;
5837
+ var MAX_VERSION_LENGTH = 64;
5838
+ var MAX_RESUME_KEY_LENGTH = 128;
5839
+ var MAX_SERVER_TIME_LENGTH = 64;
5840
+ var MAX_PROGRESS_MESSAGE_LENGTH = 500;
5841
+ var MAX_DRAIN_GRACE_SEC = 3600;
5842
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5843
+ var EXTENSION_TYPE_PATTERN = /^[a-z][a-zA-Z0-9]{0,31}(?:\.[a-z][a-zA-Z0-9-]{0,31}){1,3}$/;
5844
+ var RESUME_KEY_PATTERN = /^[\x21-\x7e]+$/;
5845
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
5846
+ var CONTROL_TYPES = new Set(FLEET_LINK_CONTROL_TYPES);
5847
+ var KIND_JSON = FLEET_LINK_FRAME_KINDS.JSON;
5848
+ var utf8Encoder = new TextEncoder();
5849
+ function utf8Decode(bytes) {
5850
+ try {
5851
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
5852
+ } catch {
5853
+ return null;
5854
+ }
5855
+ }
5856
+ __name(utf8Decode, "utf8Decode");
5857
+ function isRecord4(value) {
5858
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5859
+ }
5860
+ __name(isRecord4, "isRecord4");
5861
+ function isChannelNumber(value, allowControl = false) {
5862
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= (allowControl ? 0 : 1) && value <= FLEET_LINK_MAX_CHANNEL_NUMBER;
5863
+ }
5864
+ __name(isChannelNumber, "isChannelNumber");
5865
+ function isBoundedInt3(value, min, max) {
5866
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
5867
+ }
5868
+ __name(isBoundedInt3, "isBoundedInt3");
5869
+ function boundedString(value, max) {
5870
+ return typeof value === "string" && value.length <= max ? value : null;
5871
+ }
5872
+ __name(boundedString, "boundedString");
5873
+ function toBytes(raw) {
5874
+ if (raw instanceof Uint8Array) return raw;
5875
+ if (raw instanceof ArrayBuffer) return new Uint8Array(raw);
5876
+ if (ArrayBuffer.isView(raw)) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
5877
+ return null;
5878
+ }
5879
+ __name(toBytes, "toBytes");
5880
+ function encodeFleetLinkFrame(channel, kind, payload) {
5881
+ if (!isChannelNumber(channel, true) || !isFleetLinkFrameKind(kind) || !(payload instanceof Uint8Array)) {
5882
+ return null;
5883
+ }
5884
+ const total = FLEET_LINK_FRAME_HEADER_BYTES + payload.byteLength;
5885
+ if (total > FLEET_LINK_MAX_FRAME_BYTES) return null;
5886
+ const out = new Uint8Array(total);
5887
+ const view = new DataView(out.buffer);
5888
+ view.setUint32(0, channel, false);
5889
+ view.setUint8(4, kind);
5890
+ view.setUint32(5, payload.byteLength, false);
5891
+ out.set(payload, FLEET_LINK_FRAME_HEADER_BYTES);
5892
+ return out;
5893
+ }
5894
+ __name(encodeFleetLinkFrame, "encodeFleetLinkFrame");
5895
+ function decodeFleetLinkFrame(raw) {
5896
+ const bytes = toBytes(raw);
5897
+ if (!bytes) return null;
5898
+ if (bytes.byteLength > FLEET_LINK_MAX_FRAME_BYTES) return null;
5899
+ if (bytes.byteLength < FLEET_LINK_FRAME_HEADER_BYTES) return null;
5900
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
5901
+ const channel = view.getUint32(0, false);
5902
+ const kind = view.getUint8(4);
5903
+ const length = view.getUint32(5, false);
5904
+ if (!isFleetLinkFrameKind(kind)) return null;
5905
+ if (length !== bytes.byteLength - FLEET_LINK_FRAME_HEADER_BYTES) return null;
5906
+ return {
5907
+ channel,
5908
+ kind,
5909
+ payload: bytes.subarray(FLEET_LINK_FRAME_HEADER_BYTES)
5910
+ };
5911
+ }
5912
+ __name(decodeFleetLinkFrame, "decodeFleetLinkFrame");
5913
+ function peekFleetLinkFrameChannel(raw) {
5914
+ const bytes = toBytes(raw);
5915
+ if (!bytes || bytes.byteLength < 4) return null;
5916
+ return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, false);
5917
+ }
5918
+ __name(peekFleetLinkFrameChannel, "peekFleetLinkFrameChannel");
5919
+ function depthWithin(value, maxDepth) {
5920
+ const stack = [{ value, depth: 1 }];
5921
+ while (stack.length > 0) {
5922
+ const next = stack.pop();
5923
+ if (next.depth > maxDepth) return false;
5924
+ if (typeof next.value === "object" && next.value !== null) {
5925
+ for (const child of Object.values(next.value)) {
5926
+ if (typeof child === "object" && child !== null) stack.push({ value: child, depth: next.depth + 1 });
5927
+ }
5928
+ }
5929
+ }
5930
+ return true;
5931
+ }
5932
+ __name(depthWithin, "depthWithin");
5933
+ function decodeFleetLinkJson(payload, maxBytes = FLEET_LINK_MAX_JSON_BYTES) {
5934
+ if (!(payload instanceof Uint8Array) || payload.byteLength === 0 || payload.byteLength > maxBytes) return null;
5935
+ const text = utf8Decode(payload);
5936
+ if (text === null) return null;
5937
+ let parsed;
5938
+ try {
5939
+ parsed = JSON.parse(text, (key, value) => FORBIDDEN_KEYS.has(key) ? void 0 : value);
5940
+ } catch {
5941
+ return null;
5942
+ }
5943
+ if (!isRecord4(parsed) || !depthWithin(parsed, FLEET_LINK_MAX_JSON_DEPTH)) return null;
5944
+ return parsed;
5945
+ }
5946
+ __name(decodeFleetLinkJson, "decodeFleetLinkJson");
5947
+ function encodeFleetLinkJson(channel, value, maxBytes = channel === 0 ? FLEET_LINK_MAX_JSON_BYTES : FLEET_LINK_MAX_FRAME_BYTES - FLEET_LINK_FRAME_HEADER_BYTES) {
5948
+ let text;
5949
+ try {
5950
+ text = JSON.stringify(value);
5951
+ } catch {
5952
+ return null;
5953
+ }
5954
+ if (typeof text !== "string") return null;
5955
+ const payload = utf8Encoder.encode(text);
5956
+ if (payload.byteLength > maxBytes) return null;
5957
+ return encodeFleetLinkFrame(channel, KIND_JSON, payload);
5958
+ }
5959
+ __name(encodeFleetLinkJson, "encodeFleetLinkJson");
5960
+ function normalizeFleetLinkError(value) {
5961
+ if (!isRecord4(value) || !isFleetLinkErrorCode(value.code)) return null;
5962
+ const hint = typeof value.hint === "string" ? value.hint.slice(0, FLEET_LINK_MAX_HINT_LENGTH) : "";
5963
+ const error = { code: value.code, hint };
5964
+ if (isRecord4(value.details)) error.details = value.details;
5965
+ return error;
5966
+ }
5967
+ __name(normalizeFleetLinkError, "normalizeFleetLinkError");
5968
+ function makeFleetLinkError(code, hint, details) {
5969
+ const error = { code, hint: hint.slice(0, FLEET_LINK_MAX_HINT_LENGTH) };
5970
+ if (details) error.details = details;
5971
+ return error;
5972
+ }
5973
+ __name(makeFleetLinkError, "makeFleetLinkError");
5974
+ function closeReason(value) {
5975
+ if (value === "normal") return "normal";
5976
+ return isFleetLinkErrorCode(value) ? value : null;
5977
+ }
5978
+ __name(closeReason, "closeReason");
5979
+ function decodeCapabilities(value) {
5980
+ if (!Array.isArray(value) || value.length > MAX_TAGS) return null;
5981
+ const out = [];
5982
+ for (const tag of value) {
5983
+ if (typeof tag !== "string" || tag.length === 0 || tag.length > MAX_TAG_LENGTH) return null;
5984
+ out.push(tag);
5985
+ }
5986
+ return out;
5987
+ }
5988
+ __name(decodeCapabilities, "decodeCapabilities");
5989
+ function decodeResumeChannels(value) {
5990
+ if (!Array.isArray(value) || value.length > FLEET_LINK_MAX_CHANNELS) return null;
5991
+ const out = [];
5992
+ for (const entry of value) {
5993
+ if (!isRecord4(entry) || !isChannelNumber(entry.channel) || !isFleetLinkFamily(entry.family)) return null;
5994
+ const channel = { channel: entry.channel, family: entry.family };
5995
+ if (entry.resumeKey !== void 0) {
5996
+ const key = boundedString(entry.resumeKey, MAX_RESUME_KEY_LENGTH);
5997
+ if (key === null || !RESUME_KEY_PATTERN.test(key)) return null;
5998
+ channel.resumeKey = key;
5999
+ }
6000
+ out.push(channel);
6001
+ }
6002
+ return out;
6003
+ }
6004
+ __name(decodeResumeChannels, "decodeResumeChannels");
6005
+ function decodeChannelList(value) {
6006
+ if (!Array.isArray(value) || value.length > FLEET_LINK_MAX_CHANNELS) return null;
6007
+ return value.every((entry) => isChannelNumber(entry)) ? [...value] : null;
6008
+ }
6009
+ __name(decodeChannelList, "decodeChannelList");
6010
+ function decodeLimits(value) {
6011
+ if (!isRecord4(value)) return null;
6012
+ const { maxFrameBytes, maxChannels, pingIntervalMs, silenceMs, inlineReplyMs } = value;
6013
+ if (!isBoundedInt3(maxFrameBytes, FLEET_LINK_FRAME_HEADER_BYTES, FLEET_LINK_MAX_FRAME_BYTES) || !isBoundedInt3(maxChannels, 1, FLEET_LINK_MAX_CHANNELS) || !isBoundedInt3(pingIntervalMs, 1, 36e5) || !isBoundedInt3(silenceMs, 1, 36e5) || !isBoundedInt3(inlineReplyMs, 1, 36e5)) {
6014
+ return null;
6015
+ }
6016
+ return { maxFrameBytes, maxChannels, pingIntervalMs, silenceMs, inlineReplyMs };
6017
+ }
6018
+ __name(decodeLimits, "decodeLimits");
6019
+ function decodeHello(record) {
6020
+ const nodeId = boundedString(record.nodeId, 36);
6021
+ const token = boundedString(record.token, MAX_TOKEN_LENGTH);
6022
+ const capabilities = decodeCapabilities(record.capabilities);
6023
+ const consents = normalizeFleetConsents(record.consents);
6024
+ if (nodeId === null || !UUID_PATTERN.test(nodeId) || !isBoundedInt3(record.linkVersion, 1, 1e3) || capabilities === null || consents === null || token === null || token.length === 0) {
6025
+ return null;
6026
+ }
6027
+ const hello = {
6028
+ type: "link.hello",
6029
+ nodeId: nodeId.toLowerCase(),
6030
+ linkVersion: record.linkVersion,
6031
+ capabilities,
6032
+ consents,
6033
+ token
6034
+ };
6035
+ if (record.nodeVersion !== void 0) {
6036
+ const version = boundedString(record.nodeVersion, MAX_VERSION_LENGTH);
6037
+ if (version === null) return null;
6038
+ hello.nodeVersion = version;
6039
+ }
6040
+ if (record.resume !== void 0) {
6041
+ if (!isRecord4(record.resume) || !isBoundedInt3(record.resume.linkGeneration, 0, Number.MAX_SAFE_INTEGER)) {
6042
+ return null;
6043
+ }
6044
+ const channels = decodeResumeChannels(record.resume.channels ?? []);
6045
+ if (channels === null) return null;
6046
+ hello.resume = { linkGeneration: record.resume.linkGeneration, channels };
6047
+ }
6048
+ return hello;
6049
+ }
6050
+ __name(decodeHello, "decodeHello");
6051
+ function decodeKnownControl(record) {
6052
+ switch (record.type) {
6053
+ case "link.hello":
6054
+ return decodeHello(record);
6055
+ case "link.ready": {
6056
+ const limits = decodeLimits(record.limits);
6057
+ const serverTime = boundedString(record.serverTime, MAX_SERVER_TIME_LENGTH);
6058
+ const resumed = decodeChannelList(record.resumed ?? []);
6059
+ const closed = decodeChannelList(record.closed ?? []);
6060
+ if (!isBoundedInt3(record.linkGeneration, 1, Number.MAX_SAFE_INTEGER) || limits === null || serverTime === null || resumed === null || closed === null) {
6061
+ return null;
6062
+ }
6063
+ return { type: "link.ready", linkGeneration: record.linkGeneration, limits, serverTime, resumed, closed };
6064
+ }
6065
+ case "link.ping":
6066
+ case "link.pong":
6067
+ return typeof record.t === "number" && Number.isFinite(record.t) ? { type: record.type === "link.ping" ? "link.ping" : "link.pong", t: record.t } : null;
6068
+ case "link.pause":
6069
+ return record.reason === "kill_switch" ? { type: "link.pause", reason: "kill_switch" } : null;
6070
+ case "link.resume":
6071
+ return { type: "link.resume" };
6072
+ case "link.drain":
6073
+ return isBoundedInt3(record.graceSec, 0, MAX_DRAIN_GRACE_SEC) ? { type: "link.drain", graceSec: record.graceSec } : null;
6074
+ case "channel.open": {
6075
+ if (!isChannelNumber(record.channel) || !isFleetLinkFamily(record.family) || !isFleetLinkId(record.requestId) || record.params !== void 0 && !isRecord4(record.params)) {
6076
+ return null;
6077
+ }
6078
+ return {
6079
+ type: "channel.open",
6080
+ channel: record.channel,
6081
+ family: record.family,
6082
+ params: record.params ?? {},
6083
+ requestId: record.requestId
6084
+ };
6085
+ }
6086
+ case "channel.ready": {
6087
+ if (!isChannelNumber(record.channel) || !isFleetLinkId(record.requestId)) return null;
6088
+ const ready = {
6089
+ type: "channel.ready",
6090
+ channel: record.channel,
6091
+ requestId: record.requestId
6092
+ };
6093
+ if (record.resumeKey !== void 0) {
6094
+ const key = boundedString(record.resumeKey, MAX_RESUME_KEY_LENGTH);
6095
+ if (key === null || !RESUME_KEY_PATTERN.test(key)) return null;
6096
+ ready.resumeKey = key;
6097
+ }
6098
+ return ready;
6099
+ }
6100
+ case "channel.refused": {
6101
+ const error = normalizeFleetLinkError(
6102
+ record.error !== void 0 ? record.error : { code: record.code, hint: record.hint, details: record.details }
6103
+ );
6104
+ if (!isChannelNumber(record.channel) || !isFleetLinkId(record.requestId) || error === null) return null;
6105
+ return { type: "channel.refused", channel: record.channel, requestId: record.requestId, error };
6106
+ }
6107
+ case "channel.close": {
6108
+ const reason = closeReason(record.reason);
6109
+ if (!isChannelNumber(record.channel) || reason === null) return null;
6110
+ const close = { type: "channel.close", channel: record.channel, reason };
6111
+ if (typeof record.hint === "string") close.hint = record.hint.slice(0, FLEET_LINK_MAX_HINT_LENGTH);
6112
+ return close;
6113
+ }
6114
+ case "operation.progress": {
6115
+ if (!isFleetLinkId(record.operationId) || !isRecord4(record.progress)) return null;
6116
+ const progress = {};
6117
+ if (record.progress.percent !== void 0) {
6118
+ const percent2 = record.progress.percent;
6119
+ if (typeof percent2 !== "number" || !Number.isFinite(percent2) || percent2 < 0 || percent2 > 100)
6120
+ return null;
6121
+ progress.percent = percent2;
6122
+ }
6123
+ if (record.progress.message !== void 0) {
6124
+ if (typeof record.progress.message !== "string") return null;
6125
+ progress.message = record.progress.message.slice(0, MAX_PROGRESS_MESSAGE_LENGTH);
6126
+ }
6127
+ return { type: "operation.progress", operationId: record.operationId, progress };
6128
+ }
6129
+ case "operation.done": {
6130
+ if (!isFleetLinkId(record.operationId)) return null;
6131
+ const done = { type: "operation.done", operationId: record.operationId };
6132
+ if (record.error !== void 0) {
6133
+ const error = normalizeFleetLinkError(record.error);
6134
+ if (error === null) return null;
6135
+ done.error = error;
6136
+ } else if (record.result !== void 0) {
6137
+ done.result = record.result;
6138
+ }
6139
+ return done;
6140
+ }
6141
+ case "operation.cancel":
6142
+ return isFleetLinkId(record.operationId) ? { type: "operation.cancel", operationId: record.operationId } : null;
6143
+ default:
6144
+ return null;
6145
+ }
6146
+ }
6147
+ __name(decodeKnownControl, "decodeKnownControl");
6148
+ function decodeFleetLinkControl(payload) {
6149
+ const record = decodeFleetLinkJson(payload);
6150
+ if (!record || typeof record.type !== "string") return null;
6151
+ if (CONTROL_TYPES.has(record.type)) return decodeKnownControl(record);
6152
+ if (!EXTENSION_TYPE_PATTERN.test(record.type)) return null;
6153
+ return record;
6154
+ }
6155
+ __name(decodeFleetLinkControl, "decodeFleetLinkControl");
6156
+ function isFleetLinkControlMessage(message) {
6157
+ return CONTROL_TYPES.has(message.type);
6158
+ }
6159
+ __name(isFleetLinkControlMessage, "isFleetLinkControlMessage");
6160
+ function encodeFleetLinkControl(message) {
6161
+ return encodeFleetLinkJson(0, message, FLEET_LINK_MAX_JSON_BYTES);
6162
+ }
6163
+ __name(encodeFleetLinkControl, "encodeFleetLinkControl");
6164
+ function asFleetLinkRequest(record) {
6165
+ if (!isFleetLinkId(record.requestId) || typeof record.type !== "string" || record.type.length > 128) return null;
6166
+ if (record.params !== void 0 && !isRecord4(record.params)) return null;
6167
+ const request = { requestId: record.requestId, type: record.type };
6168
+ if (record.params !== void 0) request.params = record.params;
6169
+ return request;
6170
+ }
6171
+ __name(asFleetLinkRequest, "asFleetLinkRequest");
6172
+ function decodeFleetLinkAck(payload) {
6173
+ if (!(payload instanceof Uint8Array) || payload.byteLength !== 4) return null;
6174
+ return new DataView(payload.buffer, payload.byteOffset, 4).getUint32(0, false);
6175
+ }
6176
+ __name(decodeFleetLinkAck, "decodeFleetLinkAck");
5557
6177
  var FLEET_JOB_FAILURE_KIND_MODEL_USAGE_LIMIT = "model_usage_limit";
5558
6178
  var FLEET_MODEL_USAGE_LIMIT_FALLBACK_MS = 60 * 60 * 1e3;
5559
6179
  var FLEET_MODEL_USAGE_LIMIT_MIN_MS = 60 * 1e3;
@@ -5905,10 +6525,109 @@ var FLEET_CREDENTIAL_MAX_LENGTH = 256;
5905
6525
  var FLEET_DEFAULT_ENROLLMENT_TOKEN_TTL_MS = 15 * 6e4;
5906
6526
  var FLEET_DEFAULT_NODE_OFFLINE_AFTER_MS = 5 * 6e4;
5907
6527
  var FLEET_DEFAULT_NODE_OFFLINE_NOTICE_AFTER_MS = 30 * 6e4;
5908
- var FLEET_DEFAULT_MAX_CAPABILITY_TAGS = 16;
6528
+ var FLEET_DEFAULT_MAX_CAPABILITY_TAGS = 32;
6529
+ var FLEET_LEGACY_MAX_CAPABILITY_TAGS = 16;
5909
6530
  var FLEET_DEFAULT_MAX_CAPABILITY_TAG_LENGTH = 32;
5910
6531
  var FLEET_DEFAULT_CREDENTIAL_ROTATION_OVERLAP_MS = 15 * 6e4;
5911
6532
  var FLEET_MAX_CREDENTIAL_ROTATION_OVERLAP_MS = 24 * 60 * 6e4;
6533
+ var FLEET_HEALTH_PROBE_TIMEOUT_MS = 2e3;
6534
+ var FLEET_HEALTH_RAW_RETENTION_MS = 7 * 24 * 60 * 6e4;
6535
+ var FLEET_HEALTH_HOURLY_RETENTION_MS = 90 * 24 * 60 * 6e4;
6536
+ var FLEET_HEALTH_DISK_CRITICAL_MARGIN_BYTES = 1024 ** 3;
6537
+ var FLEET_HEALTH_MAX_GPU_MODEL_LENGTH = 64;
6538
+ var FLEET_HEALTH_MAX_GPUS = 16;
6539
+ var FLEET_HEALTH_MAX_MIB = 2 ** 40;
6540
+ var FLEET_HEALTH_MAX_LOAD_AVG = 1e5;
6541
+ var FLEET_HEALTH_MAX_CORES = 4096;
6542
+ var FLEET_HEALTH_MAX_UPTIME_SEC = 100 * 365 * 24 * 60 * 60;
6543
+ var FLEET_HEALTH_MAX_FAILED_SERVICES = 1e4;
6544
+ var FLEET_NODE_GPU_VENDORS = ["nvidia", "amd", "intel", "apple", "other"];
6545
+ function finiteIn(value, min, max) {
6546
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
6547
+ if (value < min || value > max) return null;
6548
+ return value;
6549
+ }
6550
+ __name(finiteIn, "finiteIn");
6551
+ function round(value, decimals) {
6552
+ const factor = 10 ** decimals;
6553
+ return Math.round(value * factor) / factor;
6554
+ }
6555
+ __name(round, "round");
6556
+ function percent(value) {
6557
+ const v = finiteIn(value, 0, 100);
6558
+ return v === null ? null : round(v, 1);
6559
+ }
6560
+ __name(percent, "percent");
6561
+ function wholeIn(value, min, max) {
6562
+ const v = finiteIn(value, min, max);
6563
+ return v === null ? null : Math.floor(v);
6564
+ }
6565
+ __name(wholeIn, "wholeIn");
6566
+ function boundedText(value, max) {
6567
+ if (typeof value !== "string") return null;
6568
+ const cleaned = value.replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/\s+/g, " ").trim();
6569
+ return cleaned ? cleaned.slice(0, max) : null;
6570
+ }
6571
+ __name(boundedText, "boundedText");
6572
+ function sanitizeOneGpu(raw) {
6573
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
6574
+ const source = raw;
6575
+ const vendor = FLEET_NODE_GPU_VENDORS.includes(source.vendor) ? source.vendor : "other";
6576
+ const gpu = {
6577
+ vendor,
6578
+ name: boundedText(source.name, FLEET_HEALTH_MAX_GPU_MODEL_LENGTH)
6579
+ };
6580
+ const util = percent(source.utilPercent);
6581
+ if (util !== null) gpu.utilPercent = util;
6582
+ const used = wholeIn(source.memUsedMiB, 0, FLEET_HEALTH_MAX_MIB);
6583
+ const total = wholeIn(source.memTotalMiB, 0, FLEET_HEALTH_MAX_MIB);
6584
+ if (used !== null && total !== null && used > total) return gpu;
6585
+ if (used !== null) gpu.memUsedMiB = used;
6586
+ if (total !== null) gpu.memTotalMiB = total;
6587
+ return gpu;
6588
+ }
6589
+ __name(sanitizeOneGpu, "sanitizeOneGpu");
6590
+ function sanitizeGpu(raw) {
6591
+ if (raw === null) return null;
6592
+ if (!Array.isArray(raw)) return void 0;
6593
+ const gpus = raw.slice(0, FLEET_HEALTH_MAX_GPUS).map(sanitizeOneGpu).filter((gpu) => gpu !== null);
6594
+ if (gpus.length === 0) return raw.length === 0 ? null : void 0;
6595
+ return gpus;
6596
+ }
6597
+ __name(sanitizeGpu, "sanitizeGpu");
6598
+ function sanitizeFleetNodeHealthSnapshot(raw) {
6599
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
6600
+ const source = raw;
6601
+ const out = {};
6602
+ const cpu = percent(source.cpuPercent);
6603
+ if (cpu !== null) out.cpuPercent = cpu;
6604
+ const mem = percent(source.memPercent);
6605
+ if (mem !== null) out.memPercent = mem;
6606
+ const memTotal = wholeIn(source.memTotalMiB, 0, FLEET_HEALTH_MAX_MIB);
6607
+ const memUsed = wholeIn(source.memUsedMiB, 0, FLEET_HEALTH_MAX_MIB);
6608
+ if (memTotal !== null) out.memTotalMiB = memTotal;
6609
+ if (memUsed !== null && (memTotal === null || memUsed <= memTotal)) out.memUsedMiB = memUsed;
6610
+ const disk = percent(source.diskPercent);
6611
+ if (disk !== null) out.diskPercent = disk;
6612
+ const diskFree = wholeIn(source.diskFreeMiB, 0, FLEET_HEALTH_MAX_MIB);
6613
+ const diskTotal = wholeIn(source.diskTotalMiB, 0, FLEET_HEALTH_MAX_MIB);
6614
+ if (diskTotal !== null) out.diskTotalMiB = diskTotal;
6615
+ if (diskFree !== null && (diskTotal === null || diskFree <= diskTotal)) out.diskFreeMiB = diskFree;
6616
+ const load = finiteIn(source.loadAvg1, 0, FLEET_HEALTH_MAX_LOAD_AVG);
6617
+ if (load !== null) out.loadAvg1 = round(load, 2);
6618
+ const cores = wholeIn(source.cpuCores, 1, FLEET_HEALTH_MAX_CORES);
6619
+ if (cores !== null) out.cpuCores = cores;
6620
+ const uptime2 = wholeIn(source.uptimeSec, 0, FLEET_HEALTH_MAX_UPTIME_SEC);
6621
+ if (uptime2 !== null) out.uptimeSec = uptime2;
6622
+ const gpu = sanitizeGpu(source.gpu);
6623
+ if (gpu !== void 0) out.gpu = gpu;
6624
+ if (typeof source.pendingReboot === "boolean") out.pendingReboot = source.pendingReboot;
6625
+ const failed = wholeIn(source.failedServices, 0, FLEET_HEALTH_MAX_FAILED_SERVICES);
6626
+ if (failed !== null) out.failedServices = failed;
6627
+ return Object.keys(out).length > 0 ? out : null;
6628
+ }
6629
+ __name(sanitizeFleetNodeHealthSnapshot, "sanitizeFleetNodeHealthSnapshot");
6630
+ var FLEET_HEALTH_BYTES_PER_MIB = 1024 * 1024;
5912
6631
  var FLEET_PUSH_CAPABILITY = "git-push";
5913
6632
  var FLEET_PUSH_MIN_GIT_VERSION = "2.31.0";
5914
6633
  var FLEET_PUSH_CREDENTIAL_USERNAME = "x-access-token";
@@ -6574,6 +7293,56 @@ var EVER_GAUZY_PRESET_SCOPES = INTEGRATION_SCOPES.filter(
6574
7293
  (scope) => !INTEGRATION_OPT_IN_SCOPES.includes(scope)
6575
7294
  );
6576
7295
 
7296
+ // src/core/link/modules/control.ts
7297
+ var FLEET_MANAGED_CAPABILITY = "managed";
7298
+ function createLinkControlState() {
7299
+ let paused = false;
7300
+ let drain = null;
7301
+ const listeners = /* @__PURE__ */ new Set();
7302
+ const notify = /* @__PURE__ */ __name(() => {
7303
+ const snapshot = { paused, drain };
7304
+ for (const listener of [...listeners]) {
7305
+ try {
7306
+ listener(snapshot);
7307
+ } catch {
7308
+ }
7309
+ }
7310
+ }, "notify");
7311
+ return {
7312
+ get paused() {
7313
+ return paused;
7314
+ },
7315
+ get drain() {
7316
+ return drain;
7317
+ },
7318
+ onChange(listener) {
7319
+ listeners.add(listener);
7320
+ return () => {
7321
+ listeners.delete(listener);
7322
+ };
7323
+ },
7324
+ pause() {
7325
+ if (paused) return;
7326
+ paused = true;
7327
+ notify();
7328
+ },
7329
+ resume() {
7330
+ if (!paused) return;
7331
+ paused = false;
7332
+ notify();
7333
+ },
7334
+ startDrain(message, receivedAt) {
7335
+ drain = { graceSec: message.graceSec, receivedAt };
7336
+ notify();
7337
+ }
7338
+ };
7339
+ }
7340
+ __name(createLinkControlState, "createLinkControlState");
7341
+ var controlModule = {
7342
+ id: "control",
7343
+ tags: /* @__PURE__ */ __name((environment) => environment.consents.manage ? [FLEET_MANAGED_CAPABILITY] : [], "tags")
7344
+ };
7345
+
6577
7346
  // src/core/gpu-probe.ts
6578
7347
  var VENDOR_PATTERNS = [
6579
7348
  { vendor: "nvidia", pattern: /\b(nvidia|geforce|quadro|tesla|rtx|gtx)\b/i },
@@ -6652,6 +7421,11 @@ function extractChipsetLines(stdout) {
6652
7421
  }
6653
7422
  __name(extractChipsetLines, "extractChipsetLines");
6654
7423
  async function detectGpu(runner, platform) {
7424
+ return (await detectGpuWithEvidence(runner, platform)).gpu;
7425
+ }
7426
+ __name(detectGpu, "detectGpu");
7427
+ async function detectGpuWithEvidence(runner, platform) {
7428
+ let answered = false;
6655
7429
  for (const probe of gpuProbesFor(platform)) {
6656
7430
  let stdout;
6657
7431
  try {
@@ -6663,6 +7437,7 @@ async function detectGpu(runner, platform) {
6663
7437
  } catch {
6664
7438
  continue;
6665
7439
  }
7440
+ if (probe.command !== "nvidia-smi") answered = true;
6666
7441
  let text = stdout;
6667
7442
  if (probe.command === "lspci") {
6668
7443
  text = extractDisplayLines(stdout);
@@ -6673,11 +7448,82 @@ async function detectGpu(runner, platform) {
6673
7448
  if (!model) {
6674
7449
  continue;
6675
7450
  }
6676
- return { vendor: classifyGpuVendor(text), model };
7451
+ return { gpu: { vendor: classifyGpuVendor(text), model }, answered: true };
6677
7452
  }
6678
- return null;
7453
+ return { gpu: null, answered };
6679
7454
  }
6680
- __name(detectGpu, "detectGpu");
7455
+ __name(detectGpuWithEvidence, "detectGpuWithEvidence");
7456
+ var NVIDIA_SMI_HEALTH_ARGS = [
7457
+ "--query-gpu=name,utilization.gpu,memory.used,memory.total",
7458
+ "--format=csv,noheader,nounits"
7459
+ ];
7460
+ function numericField(text) {
7461
+ if (typeof text !== "string") return void 0;
7462
+ const trimmed = text.trim();
7463
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) return void 0;
7464
+ const value = Number(trimmed);
7465
+ return Number.isFinite(value) ? value : void 0;
7466
+ }
7467
+ __name(numericField, "numericField");
7468
+ function parseNvidiaSmiHealth(stdout) {
7469
+ const readings = [];
7470
+ for (const raw of (stdout ?? "").split(/\r?\n/)) {
7471
+ const reading = parseNvidiaSmiLine(raw.trim());
7472
+ if (reading) readings.push(reading);
7473
+ }
7474
+ return readings;
7475
+ }
7476
+ __name(parseNvidiaSmiHealth, "parseNvidiaSmiHealth");
7477
+ function parseNvidiaSmiLine(line) {
7478
+ if (!line) return null;
7479
+ const cells = line.split(",");
7480
+ if (cells.length < 4) return null;
7481
+ const memTotal = numericField(cells[cells.length - 1]);
7482
+ const memUsed = numericField(cells[cells.length - 2]);
7483
+ const util = numericField(cells[cells.length - 3]);
7484
+ const name = cells.slice(0, cells.length - 3).join(",").trim().slice(0, 120);
7485
+ if (!name) return null;
7486
+ const reading = { name };
7487
+ if (util !== void 0) reading.utilPercent = util;
7488
+ if (memUsed !== void 0) reading.memUsedMiB = Math.round(memUsed);
7489
+ if (memTotal !== void 0) reading.memTotalMiB = Math.round(memTotal);
7490
+ return reading;
7491
+ }
7492
+ __name(parseNvidiaSmiLine, "parseNvidiaSmiLine");
7493
+ var ROCM_SMI_HEALTH_ARGS = ["--showuse", "--showmeminfo", "vram", "--json"];
7494
+ function parseRocmSmiHealth(stdout) {
7495
+ let parsed;
7496
+ try {
7497
+ parsed = JSON.parse((stdout ?? "").trim());
7498
+ } catch {
7499
+ return null;
7500
+ }
7501
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
7502
+ const card = Object.entries(parsed).find(([key]) => /^card\d+$/i.test(key))?.[1];
7503
+ if (!card || typeof card !== "object") return null;
7504
+ const fields = Object.entries(card);
7505
+ const read = /* @__PURE__ */ __name((pattern) => {
7506
+ const entry = fields.find(([key]) => pattern.test(key));
7507
+ return entry ? numericField(String(entry[1])) : void 0;
7508
+ }, "read");
7509
+ const util = read(/gpu use \(%\)/i);
7510
+ const totalBytes = read(/vram total memory \(b\)/i);
7511
+ const usedBytes = read(/vram total used memory \(b\)/i);
7512
+ const reading = {};
7513
+ if (util !== void 0) reading.utilPercent = util;
7514
+ if (usedBytes !== void 0) reading.memUsedMiB = Math.round(usedBytes / (1024 * 1024));
7515
+ if (totalBytes !== void 0) reading.memTotalMiB = Math.round(totalBytes / (1024 * 1024));
7516
+ return Object.keys(reading).length > 0 ? reading : null;
7517
+ }
7518
+ __name(parseRocmSmiHealth, "parseRocmSmiHealth");
7519
+ var IOREG_GPU_ARGS = ["-r", "-d", "1", "-w", "0", "-c", "IOAccelerator"];
7520
+ function parseIoregGpuUtil(stdout) {
7521
+ const match = /"Device Utilization %"\s*=\s*(\d+)/.exec(stdout ?? "");
7522
+ if (!match) return null;
7523
+ const value = Number(match[1]);
7524
+ return Number.isFinite(value) ? value : null;
7525
+ }
7526
+ __name(parseIoregGpuUtil, "parseIoregGpuUtil");
6681
7527
 
6682
7528
  // src/core/screen/capture-backend.ts
6683
7529
  var HEADLESS_BROWSER_CAPTURE_AVAILABILITY = Object.freeze({
@@ -6708,7 +7554,7 @@ __name(safeAvailable, "safeAvailable");
6708
7554
  var MAX_CAPABILITY_TAGS = FLEET_DEFAULT_MAX_CAPABILITY_TAGS;
6709
7555
  var MAX_CAPABILITY_TAG_LENGTH = FLEET_DEFAULT_MAX_CAPABILITY_TAG_LENGTH;
6710
7556
  var MAX_PLATFORM_LENGTH = FLEET_MAX_PLATFORM_LENGTH;
6711
- var MAX_VERSION_LENGTH = FLEET_MAX_VERSION_LENGTH;
7557
+ var MAX_VERSION_LENGTH2 = FLEET_MAX_VERSION_LENGTH;
6712
7558
  var MIN_CREDENTIAL_LENGTH = FLEET_CREDENTIAL_MIN_LENGTH;
6713
7559
  var MAX_CREDENTIAL_LENGTH = FLEET_CREDENTIAL_MAX_LENGTH;
6714
7560
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
@@ -6837,6 +7683,20 @@ function clampWorkspaceGcPolicy(input) {
6837
7683
  };
6838
7684
  }
6839
7685
  __name(clampWorkspaceGcPolicy, "clampWorkspaceGcPolicy");
7686
+ var DEFAULT_NODE_EXECUTION_POLICY = Object.freeze({
7687
+ allowSkipPermissions: false,
7688
+ allowShellCommands: false,
7689
+ restrictedMode: true
7690
+ });
7691
+ function resolveNodeExecutionPolicy(input) {
7692
+ if (!input || typeof input !== "object") return { ...DEFAULT_NODE_EXECUTION_POLICY };
7693
+ return {
7694
+ allowSkipPermissions: input.allowSkipPermissions === true,
7695
+ allowShellCommands: input.allowShellCommands === true,
7696
+ restrictedMode: input.restrictedMode !== false
7697
+ };
7698
+ }
7699
+ __name(resolveNodeExecutionPolicy, "resolveNodeExecutionPolicy");
6840
7700
  function redactConfig(config) {
6841
7701
  const redacted = {
6842
7702
  apiUrl: config.apiUrl,
@@ -6848,7 +7708,8 @@ function redactConfig(config) {
6848
7708
  enrolledAt: config.enrolledAt,
6849
7709
  hasSecret: typeof config.secret === "string" && config.secret.length > 0,
6850
7710
  secretStorage: config.secretStorage ?? "file",
6851
- paused: config.paused === true
7711
+ paused: config.paused === true,
7712
+ executionPolicy: resolveNodeExecutionPolicy(config.executionPolicy)
6852
7713
  };
6853
7714
  if (config.capabilitySelection !== void 0) {
6854
7715
  redacted.capabilitySelection = [...config.capabilitySelection];
@@ -7008,7 +7869,10 @@ async function detectCapabilities(runner, environment) {
7008
7869
  // SAME rule the lane used, and the two cannot disagree. `input` is
7009
7870
  // NOT advertised: nothing on this node injects input yet.
7010
7871
  environment.attended === true ? FLEET_ATTENDED_CAPABILITY : null,
7011
- environment.attended === true && isScreenCaptureAvailable(environment, environment.captureBackends) ? FLEET_SCREEN_CAPABILITY : null
7872
+ environment.attended === true && isScreenCaptureAvailable(environment, environment.captureBackends) ? FLEET_SCREEN_CAPABILITY : null,
7873
+ // Fleet node link (FL-02): the module tags, LAST, so a node that already
7874
+ // advertised everything above keeps advertising exactly that.
7875
+ ...environment.linkModuleTags ?? []
7012
7876
  ]);
7013
7877
  }
7014
7878
  __name(detectCapabilities, "detectCapabilities");
@@ -7023,7 +7887,10 @@ var NON_SELECTABLE_TAGS = /* @__PURE__ */ new Set([
7023
7887
  // A capability selection written before these tags existed must not
7024
7888
  // silently withhold a live view its owner just switched on.
7025
7889
  FLEET_ATTENDED_CAPABILITY,
7026
- FLEET_SCREEN_CAPABILITY
7890
+ FLEET_SCREEN_CAPABILITY,
7891
+ // Fleet node link (FL-02): `managed` exists only under `--manage`, which IS
7892
+ // the opt-in — a selection written before the tag existed must not withhold it.
7893
+ FLEET_MANAGED_CAPABILITY
7027
7894
  ]);
7028
7895
  function isAlwaysAdvertisedCapability(tag) {
7029
7896
  return isIdentityCapability(tag) || NON_SELECTABLE_TAGS.has(tag);
@@ -7038,10 +7905,11 @@ function applyCapabilitySelection(detected, selection) {
7038
7905
  }
7039
7906
  __name(applyCapabilitySelection, "applyCapabilitySelection");
7040
7907
  async function describeSelf(runner, environment, version, selection, telemetry = {}) {
7908
+ const healthReading = resolveTelemetry(telemetry.health);
7041
7909
  const detected = await detectCapabilities(runner, environment);
7042
7910
  const description = {
7043
7911
  platform: describePlatform(environment),
7044
- version: version.slice(0, MAX_VERSION_LENGTH),
7912
+ version: version.slice(0, MAX_VERSION_LENGTH2),
7045
7913
  capabilities: applyCapabilitySelection(detected, selection)
7046
7914
  };
7047
7915
  const cliVersion = await resolveTelemetry(telemetry.cliVersion);
@@ -7093,6 +7961,19 @@ async function describeSelf(runner, environment, version, selection, telemetry =
7093
7961
  description.maxCpuPercent = typeof limits.maxCpuPercent === "number" ? limits.maxCpuPercent : null;
7094
7962
  description.maxMemoryMb = typeof limits.maxMemoryMb === "number" ? limits.maxMemoryMb : null;
7095
7963
  }
7964
+ const health2 = sanitizeFleetNodeHealthSnapshot(await healthReading);
7965
+ if (health2) {
7966
+ description.health = health2;
7967
+ }
7968
+ const consents = await resolveTelemetry(telemetry.consents);
7969
+ if (consents) {
7970
+ description.consents = {
7971
+ work: consents.work === true,
7972
+ attend: consents.attend === true,
7973
+ manage: consents.manage === true,
7974
+ usageScan: consents.usageScan === true
7975
+ };
7976
+ }
7096
7977
  return description;
7097
7978
  }
7098
7979
  __name(describeSelf, "describeSelf");
@@ -7188,6 +8069,9 @@ function parseConfig(raw) {
7188
8069
  if (typeof candidate.workspaceRoot === "string" && ABSOLUTE_PATH.test(candidate.workspaceRoot.trim())) {
7189
8070
  config.workspaceRoot = candidate.workspaceRoot.trim();
7190
8071
  }
8072
+ if (candidate.executionPolicy && typeof candidate.executionPolicy === "object") {
8073
+ config.executionPolicy = resolveNodeExecutionPolicy(candidate.executionPolicy);
8074
+ }
7191
8075
  if (candidate.unsafe && typeof candidate.unsafe === "object" && typeof candidate.unsafe.since === "string" && Number.isFinite(Date.parse(candidate.unsafe.since)) && typeof candidate.unsafe.reason === "string" && candidate.unsafe.reason.trim()) {
7192
8076
  config.unsafe = {
7193
8077
  since: new Date(candidate.unsafe.since).toISOString(),
@@ -7624,6 +8508,17 @@ function selfDescription(source) {
7624
8508
  if (source.maxMemoryMb !== void 0) {
7625
8509
  out.maxMemoryMb = source.maxMemoryMb;
7626
8510
  }
8511
+ if (source.health !== void 0) {
8512
+ out.health = source.health;
8513
+ }
8514
+ if (source.consents !== void 0) {
8515
+ out.consents = {
8516
+ work: source.consents.work === true,
8517
+ attend: source.consents.attend === true,
8518
+ manage: source.consents.manage === true,
8519
+ usageScan: source.consents.usageScan === true
8520
+ };
8521
+ }
7627
8522
  return out;
7628
8523
  }
7629
8524
  __name(selfDescription, "selfDescription");
@@ -7692,7 +8587,18 @@ var systemScheduler = {
7692
8587
  clearTimeout: /* @__PURE__ */ __name((handle) => clearTimeout(handle), "clearTimeout")
7693
8588
  };
7694
8589
  var OPTIONAL_DESCRIPTION_FIELD_TIERS = [
7695
- // NEWEST first. Node lifecycle + remote node limits (self-build slices
8590
+ // NEWEST first. Computer health (FL-03): the health snapshot. Its own
8591
+ // tier, so a platform that predates it costs exactly this one field —
8592
+ // the consents, pinned CLI versions and enforced limits below keep
8593
+ // flowing (FR-3, U9). Newer than the consents: FL-02 reached `develop`
8594
+ // first, so a platform that knows the consents but not the snapshot
8595
+ // exists, and the reverse never will.
8596
+ ["health"],
8597
+ // Fleet node link (FL-02): the consents. A platform that predates them
8598
+ // answers 400; the node strips this tier, keeps beating with everything
8599
+ // else, and does not open the link (spec U11).
8600
+ ["consents"],
8601
+ // Then Node lifecycle + remote node limits (self-build slices
7696
8602
  // AR, AS). Tiered (review): `cliVersions` goes out on EVERY beat of a
7697
8603
  // node with a pinned CLI, so an upgraded node talking to a platform that
7698
8604
  // predates it would otherwise drop the worker state and housekeeping
@@ -7757,6 +8663,12 @@ var HeartbeatLoop = class {
7757
8663
  * will never accept them.
7758
8664
  */
7759
8665
  strippedTiers = 0;
8666
+ /**
8667
+ * Fleet node link (FL-02): the platform refused more than the legacy
8668
+ * {@link FLEET_LEGACY_MAX_CAPABILITY_TAGS} tags; beats carry the first
8669
+ * sixteen from then on. Process-scoped for the same reason as the tiers.
8670
+ */
8671
+ legacyTagCap = false;
7760
8672
  state = {
7761
8673
  state: "idle",
7762
8674
  lastHeartbeatAt: null,
@@ -7779,6 +8691,17 @@ var HeartbeatLoop = class {
7779
8691
  getState() {
7780
8692
  return { ...this.state };
7781
8693
  }
8694
+ /**
8695
+ * Whether the platform has NOT proven it predates `field` — i.e. the beat
8696
+ * still carries it. The node link opens only while `consents` is still
8697
+ * accepted: a platform that rejected the field cannot authorise a channel
8698
+ * by consent, so this node does not ask it to (spec U11).
8699
+ */
8700
+ acceptsField(field) {
8701
+ return !OPTIONAL_DESCRIPTION_FIELD_TIERS.slice(0, this.strippedTiers).some(
8702
+ (tier) => tier.includes(field)
8703
+ );
8704
+ }
7782
8705
  onChange(listener) {
7783
8706
  this.listeners.add(listener);
7784
8707
  return () => {
@@ -7856,22 +8779,36 @@ var HeartbeatLoop = class {
7856
8779
  */
7857
8780
  async beat(description) {
7858
8781
  const credential = { nodeId: this.options.nodeId, secret: this.options.secret };
7859
- let sent = stripTiers(description, this.strippedTiers);
8782
+ const shape = /* @__PURE__ */ __name((tiers) => capTags(stripTiers(description, tiers), this.legacyTagCap), "shape");
8783
+ let sent = shape(this.strippedTiers);
7860
8784
  try {
7861
8785
  return await this.options.client.heartbeat({ ...credential, ...sent });
7862
8786
  } catch (error) {
7863
8787
  if (!isFieldRejection(error)) throw error;
7864
8788
  let lastError = error;
8789
+ if (!this.legacyTagCap && (description.capabilities?.length ?? 0) > FLEET_LEGACY_MAX_CAPABILITY_TAGS) {
8790
+ sent = capTags(stripTiers(description, this.strippedTiers), true);
8791
+ try {
8792
+ const result = await this.options.client.heartbeat({ ...credential, ...sent });
8793
+ this.legacyTagCap = true;
8794
+ this.options.logger?.warn(
8795
+ `Platform refused more than ${FLEET_LEGACY_MAX_CAPABILITY_TAGS} capability tags; reporting the first ${FLEET_LEGACY_MAX_CAPABILITY_TAGS} until this node restarts.`
8796
+ );
8797
+ return result;
8798
+ } catch (retryError) {
8799
+ if (!isFieldRejection(retryError)) throw retryError;
8800
+ lastError = retryError;
8801
+ this.legacyTagCap = true;
8802
+ }
8803
+ }
7865
8804
  for (let tiers = this.strippedTiers + 1; tiers <= OPTIONAL_DESCRIPTION_FIELD_TIERS.length; tiers += 1) {
7866
- const candidate = stripTiers(description, tiers);
8805
+ const candidate = shape(tiers);
7867
8806
  if (Object.keys(candidate).length === Object.keys(sent).length) continue;
7868
8807
  sent = candidate;
7869
8808
  try {
7870
8809
  const result = await this.options.client.heartbeat({ ...credential, ...candidate });
7871
8810
  this.strippedTiers = tiers;
7872
- this.options.logger?.warn(
7873
- tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length ? "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts." : "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts."
7874
- );
8811
+ this.options.logger?.warn(describeStrippedTiers(tiers));
7875
8812
  return result;
7876
8813
  } catch (retryError) {
7877
8814
  if (!isFieldRejection(retryError)) throw retryError;
@@ -7942,6 +8879,25 @@ var HeartbeatLoop = class {
7942
8879
  }
7943
8880
  }
7944
8881
  };
8882
+ function capTags(description, cap) {
8883
+ if (!cap || !Array.isArray(description.capabilities)) return description;
8884
+ if (description.capabilities.length <= FLEET_LEGACY_MAX_CAPABILITY_TAGS) return description;
8885
+ return { ...description, capabilities: description.capabilities.slice(0, FLEET_LEGACY_MAX_CAPABILITY_TAGS) };
8886
+ }
8887
+ __name(capTags, "capTags");
8888
+ function describeStrippedTiers(tiers) {
8889
+ if (tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length) {
8890
+ return "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts.";
8891
+ }
8892
+ if (tiers >= 3) {
8893
+ return "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts.";
8894
+ }
8895
+ if (tiers >= 2) {
8896
+ return "Platform rejected the consents field; it predates the node link. Still reporting pinned CLI versions, enforced limits, worker state and housekeeping; the node link stays closed and the health snapshot off until this node restarts.";
8897
+ }
8898
+ return "Platform rejected the health snapshot; it predates it. Still reporting everything else; health stays off until this node restarts.";
8899
+ }
8900
+ __name(describeStrippedTiers, "describeStrippedTiers");
7945
8901
  function stripTiers(description, tiers) {
7946
8902
  const out = {};
7947
8903
  const dropped = new Set(OPTIONAL_DESCRIPTION_FIELD_TIERS.slice(0, tiers).flat());
@@ -8114,6 +9070,8 @@ __name(cacheProbe, "cacheProbe");
8114
9070
  // src/core/executors/model-cli.ts
8115
9071
  var MODEL_CLI_EMPTY_MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
8116
9072
  `;
9073
+ var MODEL_CLI_FLEET_SETTINGS = `${JSON.stringify({ disableAllHooks: true }, null, 2)}
9074
+ `;
8117
9075
  var CODEX_SETTING_VALUE = /^[A-Za-z0-9._-]{1,64}$/;
8118
9076
  var TOML_STRING_ASSIGNMENT = /^([A-Za-z0-9_-]+)[ \t]*=[ \t]*(?:"([^"\\]*)"|'([^']*)')(?:[ \t]+#.*|#.*)?$/;
8119
9077
  var TOML_MULTILINE_VALUE_START = /^[^=#]*=[ \t]*("""|''')/;
@@ -8329,6 +9287,10 @@ function buildModelCliCommand(input) {
8329
9287
  if (execution.effort && !omitted.has("--effort")) args.push("--effort", execution.effort);
8330
9288
  if (budget && !omitted.has("--max-budget-usd")) args.push("--max-budget-usd", budget);
8331
9289
  if (execution.skipPermissions === true) args.push("--dangerously-skip-permissions");
9290
+ if (input.settingsPath !== void 0) {
9291
+ args.push("--setting-sources", "user", "--settings", quoteShellPath(input.settingsPath, platform));
9292
+ }
9293
+ if (input.restricted === true && execution.skipPermissions !== true) args.push("--restricted");
8332
9294
  if (mcp) {
8333
9295
  args.push(
8334
9296
  "--mcp-config",
@@ -8393,6 +9355,16 @@ function assertMountGrantsInCommand(input) {
8393
9355
  __name(assertMountGrantsInCommand, "assertMountGrantsInCommand");
8394
9356
  var STRICT_MCP_CONFIG_IN_COMMAND = / --mcp-config "[^"]+" --strict-mcp-config /;
8395
9357
  var CODEX_IGNORE_USER_CONFIG_IN_COMMAND = / exec --ignore-user-config /;
9358
+ var SETTINGS_ISOLATION_IN_COMMAND = / --setting-sources user --settings "[^"]+" /;
9359
+ function assertSettingsIsolationInCommand(input) {
9360
+ if (input.execution.provider !== "claude-code") return;
9361
+ if (!SETTINGS_ISOLATION_IN_COMMAND.test(input.command)) {
9362
+ throw new ModelCliCommandError(
9363
+ "The claude-code command line does not carry --setting-sources user --settings <file>, so the run would load the repository's own .claude/settings.json (its hooks, env and apiKeyHelper) and run hooks"
9364
+ );
9365
+ }
9366
+ }
9367
+ __name(assertSettingsIsolationInCommand, "assertSettingsIsolationInCommand");
8396
9368
  function assertMcpIsolationInCommand(input) {
8397
9369
  if (input.execution.provider === "codex") {
8398
9370
  if (!CODEX_IGNORE_USER_CONFIG_IN_COMMAND.test(input.command)) {
@@ -8728,9 +9700,27 @@ var MODEL_CLI_REQUIRED_FLAGS = {
8728
9700
  // since the MCP isolation fix (#2584): the empty strict config, or the
8729
9701
  // bridge's. A CLI without them cannot run fleet work at all, so `doctor`
8730
9702
  // must say INCOMPATIBLE, not "runs that need them will fail".
8731
- "claude-code": ["-p", "--output-format", "--permission-mode", "--mcp-config", "--strict-mcp-config"],
9703
+ //
9704
+ // `--setting-sources` + `--settings` ride EVERY fleet Claude Code run since
9705
+ // control 17 (2026-10-18): the worktree's own `.claude/settings*.json` is
9706
+ // never loaded and every hook is off. A CLI without them cannot keep a
9707
+ // repository's hooks from running as the machine owner, so it is
9708
+ // INCOMPATIBLE rather than "runs go without".
9709
+ "claude-code": [
9710
+ "-p",
9711
+ "--output-format",
9712
+ "--permission-mode",
9713
+ "--mcp-config",
9714
+ "--strict-mcp-config",
9715
+ "--setting-sources",
9716
+ "--settings"
9717
+ ],
8732
9718
  codex: ["--json", "--sandbox", "-C", "--ignore-user-config"]
8733
9719
  };
9720
+ var MODEL_CLI_HARDENING_FLAGS = {
9721
+ "claude-code": ["--restricted"],
9722
+ codex: []
9723
+ };
8734
9724
  var MODEL_CLI_CONDITIONAL_FLAGS = {
8735
9725
  "claude-code": [
8736
9726
  "--model",
@@ -8864,6 +9854,7 @@ function judgeModelCliHelp(provider, executable, version, help) {
8864
9854
  missingRequiredFlags: [],
8865
9855
  missingConditionalFlags: [],
8866
9856
  unsupportedOptionalFlags: [],
9857
+ missingHardeningFlags: [],
8867
9858
  note: null
8868
9859
  };
8869
9860
  if (help === null) {
@@ -8882,7 +9873,8 @@ function judgeModelCliHelp(provider, executable, version, help) {
8882
9873
  supportedFlags: advertised,
8883
9874
  missingRequiredFlags: missing(required),
8884
9875
  missingConditionalFlags: missing(MODEL_CLI_CONDITIONAL_FLAGS[provider]),
8885
- unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider])
9876
+ unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider]),
9877
+ missingHardeningFlags: missing(MODEL_CLI_HARDENING_FLAGS[provider])
8886
9878
  };
8887
9879
  }
8888
9880
  __name(judgeModelCliHelp, "judgeModelCliHelp");
@@ -8978,11 +9970,16 @@ __name(formatBytes, "formatBytes");
8978
9970
  var import_node_fs = require("node:fs");
8979
9971
  var import_node_path = require("node:path");
8980
9972
  async function measureWorkspaceFreeBytes(probe, rootPath) {
9973
+ const candidate = await nearestExistingPath(rootPath);
9974
+ return candidate === null ? null : detectDiskFreeBytes(probe, candidate);
9975
+ }
9976
+ __name(measureWorkspaceFreeBytes, "measureWorkspaceFreeBytes");
9977
+ async function nearestExistingPath(rootPath) {
8981
9978
  let candidate = rootPath;
8982
9979
  for (; ; ) {
8983
9980
  try {
8984
9981
  await import_node_fs.promises.lstat(candidate);
8985
- break;
9982
+ return candidate;
8986
9983
  } catch (error) {
8987
9984
  if (error.code !== "ENOENT") {
8988
9985
  return null;
@@ -8994,9 +9991,8 @@ async function measureWorkspaceFreeBytes(probe, rootPath) {
8994
9991
  candidate = parent;
8995
9992
  }
8996
9993
  }
8997
- return detectDiskFreeBytes(probe, candidate);
8998
9994
  }
8999
- __name(measureWorkspaceFreeBytes, "measureWorkspaceFreeBytes");
9995
+ __name(nearestExistingPath, "nearestExistingPath");
9000
9996
 
9001
9997
  // src/core/worker-loop.ts
9002
9998
  var DEFAULT_IDLE_POLL_MS = 5e3;
@@ -10593,17 +11589,22 @@ __name(keychainDisabledByEnv, "keychainDisabledByEnv");
10593
11589
  // src/node-io.ts
10594
11590
  function createCommandRunner() {
10595
11591
  return {
10596
- run: /* @__PURE__ */ __name((command, args) => new Promise((resolve9) => {
11592
+ run: /* @__PURE__ */ __name((command, args, options) => new Promise((resolve9) => {
11593
+ const timeout = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 1e4;
10597
11594
  (0, import_node_child_process.execFile)(
10598
11595
  command,
10599
11596
  args,
10600
- { shell: process.platform === "win32", windowsHide: true, timeout: 1e4 },
11597
+ { shell: process.platform === "win32", windowsHide: true, timeout },
10601
11598
  (error, stdout, stderr) => {
10602
11599
  const code = error ? error.code ?? 1 : 0;
10603
11600
  resolve9({
10604
11601
  code: typeof code === "number" ? code : 1,
10605
11602
  stdout: String(stdout),
10606
11603
  stderr: String(stderr),
11604
+ // A string code is a spawn failure (`ENOENT`: not installed),
11605
+ // not an exit status; `code` above still reads 1 for callers
11606
+ // that only ask "did it succeed".
11607
+ ...typeof code === "string" ? { spawnError: code } : {},
10607
11608
  // `execFile` sets `killed` when it terminated the child at
10608
11609
  // `timeout`; what the child printed until then is partial.
10609
11610
  timedOut: Boolean(error && error.killed)
@@ -10782,7 +11783,8 @@ function readCpuTicks() {
10782
11783
  }
10783
11784
  __name(readCpuTicks, "readCpuTicks");
10784
11785
  function createResourceProbe(options = {}) {
10785
- const windowMs = Math.max(options.sampleWindowMs ?? 0, 0);
11786
+ const everyWindowMs = Math.max(options.sampleWindowMs ?? 0, 0);
11787
+ const firstWindowMs = Math.max(options.firstSampleWindowMs ?? 0, 0);
10786
11788
  let previous = null;
10787
11789
  const measure = /* @__PURE__ */ __name((current, baseline) => {
10788
11790
  if (!baseline) return 0;
@@ -10795,6 +11797,7 @@ function createResourceProbe(options = {}) {
10795
11797
  return {
10796
11798
  sample: /* @__PURE__ */ __name(async () => {
10797
11799
  let baseline = previous;
11800
+ const windowMs = everyWindowMs > 0 ? everyWindowMs : previous === null ? firstWindowMs : 0;
10798
11801
  if (windowMs > 0) {
10799
11802
  baseline = readCpuTicks();
10800
11803
  await new Promise((resolve9) => setTimeout(resolve9, windowMs));
@@ -10823,10 +11826,51 @@ function createDiskProbe() {
10823
11826
  } catch {
10824
11827
  return null;
10825
11828
  }
10826
- }, "freeBytes")
11829
+ }, "freeBytes"),
11830
+ // Computer health (FL-03): the same `statfs`, read whole. Null on the
11831
+ // same failures, and on any figure that is not a sane byte count.
11832
+ volume: /* @__PURE__ */ __name(async (targetPath) => {
11833
+ try {
11834
+ const stats = await fsp.statfs(targetPath);
11835
+ const block = Number(stats.bsize);
11836
+ const reading = {
11837
+ totalBytes: Number(stats.blocks) * block,
11838
+ freeBytes: Number(stats.bfree) * block,
11839
+ availableBytes: Number(stats.bavail) * block
11840
+ };
11841
+ const sane = Object.values(reading).every((value) => Number.isFinite(value) && value >= 0);
11842
+ return sane && reading.totalBytes > 0 ? reading : null;
11843
+ } catch {
11844
+ return null;
11845
+ }
11846
+ }, "volume")
10827
11847
  };
10828
11848
  }
10829
11849
  __name(createDiskProbe, "createDiskProbe");
11850
+ function createHealthProbeIo(runner) {
11851
+ return {
11852
+ platform: process.platform,
11853
+ runner,
11854
+ // Its own sampler — see `createResourceProbe` on why it is never shared
11855
+ // with the admission gate — whose first reading is a real 250 ms one.
11856
+ resourceProbe: createResourceProbe({ firstSampleWindowMs: 250 }),
11857
+ host: {
11858
+ loadAvg1: /* @__PURE__ */ __name(() => os.loadavg()[0] ?? null, "loadAvg1"),
11859
+ uptimeSec: /* @__PURE__ */ __name(() => os.uptime(), "uptimeSec"),
11860
+ cores: /* @__PURE__ */ __name(() => typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length, "cores")
11861
+ },
11862
+ disk: createDiskProbe(),
11863
+ pathExists: /* @__PURE__ */ __name(async (target) => {
11864
+ try {
11865
+ await fsp.access(target);
11866
+ return true;
11867
+ } catch {
11868
+ return false;
11869
+ }
11870
+ }, "pathExists")
11871
+ };
11872
+ }
11873
+ __name(createHealthProbeIo, "createHealthProbeIo");
10830
11874
  var systemFetch = /* @__PURE__ */ __name((url, init) => fetch(url, {
10831
11875
  method: init.method,
10832
11876
  headers: init.headers,
@@ -11296,6 +12340,27 @@ var FleetJobClient = class {
11296
12340
  expiresInSec: typeof payload.expiresInSec === "number" ? payload.expiresInSec : 60
11297
12341
  };
11298
12342
  }
12343
+ /**
12344
+ * Fleet node link (FL-02) — a single-use, five-minute token for this
12345
+ * machine's one outbound link socket, minted through the node credential.
12346
+ * Never logged. A platform that does not accept links answers 404, which
12347
+ * reaches the caller as an `invalid-request` error with `status: 404`.
12348
+ */
12349
+ async mintLinkToken() {
12350
+ const payload = await this.post("api/fleet/link-token", "link-token", {
12351
+ nodeId: this.nodeId,
12352
+ secret: this.secret
12353
+ });
12354
+ if (typeof payload?.token !== "string" || !payload.token || typeof payload.wsPath !== "string") {
12355
+ throw new FleetClientError("malformed", "Link token response did not contain a token");
12356
+ }
12357
+ this.logger?.protect(payload.token);
12358
+ return {
12359
+ token: payload.token,
12360
+ wsPath: payload.wsPath,
12361
+ expiresInSec: typeof payload.expiresInSec === "number" ? payload.expiresInSec : 300
12362
+ };
12363
+ }
11299
12364
  /** Report what the watched Agent's profile holds here (counts only — never a path, never a cookie). */
11300
12365
  async reportComputerProfile(sessionId, report) {
11301
12366
  const payload = await this.post(
@@ -11393,19 +12458,287 @@ function errorForJobStatus(status, operation) {
11393
12458
  }
11394
12459
  __name(errorForJobStatus, "errorForJobStatus");
11395
12460
 
11396
- // src/core/housekeeping-report.ts
11397
- function summarizeWorkspaceReap(result) {
11398
- const dryRun = result.dryRun === true;
11399
- const worktrees = dryRun ? result.kept.length + result.removed.length : result.kept.length;
11400
- const keptBytes = result.kept.reduce((sum, verdict) => sum + safeBytes(verdict.record.sizeBytes), 0);
11401
- const keptPoolBytes = result.keptPools.reduce((sum, verdict) => sum + safeBytes(verdict.pool.sizeBytes), 0);
11402
- const plannedBytes = dryRun ? result.removed.reduce((sum, entry) => sum + safeBytes(entry.freedBytes), 0) + result.removedPools.reduce((sum, entry) => sum + safeBytes(entry.freedBytes), 0) : 0;
11403
- return {
11404
- workspaceCount: worktrees,
11405
- workspaceBytes: keptBytes + keptPoolBytes + plannedBytes,
11406
- freedBytes: dryRun ? 0 : safeBytes(result.freedBytes),
11407
- dryRun
11408
- };
12461
+ // src/core/health-probe.ts
12462
+ var BYTES_PER_MIB = 1024 * 1024;
12463
+ var DEFAULT_SLOW_FACT_TTL_MS = 5 * 6e4;
12464
+ var DEFAULT_SLOW_FACT_DEADLINE_MS = 15e3;
12465
+ var GPU_IDENTITY_TTL_MS = 10 * 6e4;
12466
+ var LINUX_REBOOT_REQUIRED_PATH = "/var/run/reboot-required";
12467
+ var LINUX_DEBIAN_RELEASE_PATH = "/etc/debian_version";
12468
+ var NEEDS_RESTARTING_REBOOT_PATTERN = /reboot is required/i;
12469
+ var WINDOWS_HEALTH_SCRIPT = [
12470
+ "$ErrorActionPreference = 'Stop'",
12471
+ "try {",
12472
+ " $pending = (Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending') -or (Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired')",
12473
+ ' "reboot=$pending"',
12474
+ "} catch {}",
12475
+ "try {",
12476
+ ` $failed = @(Get-CimInstance -ClassName Win32_Service -Filter "StartMode='Auto' AND State='Stopped' AND ExitCode<>0 AND ExitCode<>1077")`,
12477
+ ' "failed=$($failed.Count)"',
12478
+ "} catch {}",
12479
+ "try {",
12480
+ " $adapters = @(Get-CimInstance -ClassName Win32_VideoController)",
12481
+ ' "adapters=$($adapters.Count)"',
12482
+ ' foreach ($adapter in $adapters) { "gpu=$($adapter.Name)" }',
12483
+ "} catch {}"
12484
+ ].join("\n");
12485
+ function encodePowerShell(script) {
12486
+ return Buffer.from(script, "utf16le").toString("base64");
12487
+ }
12488
+ __name(encodePowerShell, "encodePowerShell");
12489
+ function parseWindowsHealth(stdout) {
12490
+ const facts = {};
12491
+ let adapterCount = null;
12492
+ const names = [];
12493
+ for (const raw of (stdout ?? "").split(/\r?\n/)) {
12494
+ const line = raw.trim();
12495
+ const eq = line.indexOf("=");
12496
+ if (eq <= 0) continue;
12497
+ const key = line.slice(0, eq);
12498
+ const value = line.slice(eq + 1).trim();
12499
+ if (key === "reboot" && /^(true|false)$/i.test(value)) facts.pendingReboot = /^true$/i.test(value);
12500
+ if (key === "failed" && /^\d+$/.test(value)) facts.failedServices = Number(value);
12501
+ if (key === "adapters" && /^\d+$/.test(value)) adapterCount = Number(value);
12502
+ if (key === "gpu" && value) names.push(value);
12503
+ }
12504
+ if (adapterCount !== null) facts.adapters = adapterCount > 0 ? names : [];
12505
+ return facts;
12506
+ }
12507
+ __name(parseWindowsHealth, "parseWindowsHealth");
12508
+ function countSystemdFailedUnits(stdout) {
12509
+ return (stdout ?? "").split(/\r?\n/).filter((line) => line.trim().length > 0).length;
12510
+ }
12511
+ __name(countSystemdFailedUnits, "countSystemdFailedUnits");
12512
+ function refreshInBackground(probe, ttlMs, now = Date.now) {
12513
+ let value = null;
12514
+ let answered = false;
12515
+ let expiresAt = Number.NEGATIVE_INFINITY;
12516
+ let inFlight = null;
12517
+ const refresh = /* @__PURE__ */ __name(() => {
12518
+ if (!inFlight) {
12519
+ inFlight = Promise.resolve().then(probe).catch(() => null).then((fresh) => {
12520
+ value = fresh;
12521
+ answered = true;
12522
+ expiresAt = now() + ttlMs;
12523
+ return fresh;
12524
+ }).finally(() => {
12525
+ inFlight = null;
12526
+ });
12527
+ }
12528
+ return inFlight;
12529
+ }, "refresh");
12530
+ return async () => {
12531
+ if (now() < expiresAt) return value;
12532
+ const pending = refresh();
12533
+ return answered ? value : pending;
12534
+ };
12535
+ }
12536
+ __name(refreshInBackground, "refreshInBackground");
12537
+ function withDeadline(work, ms) {
12538
+ return new Promise((resolve9) => {
12539
+ let settled = false;
12540
+ const timer = setTimeout(() => {
12541
+ if (settled) return;
12542
+ settled = true;
12543
+ resolve9(void 0);
12544
+ }, ms);
12545
+ timer.unref?.();
12546
+ const finish = /* @__PURE__ */ __name((value) => {
12547
+ if (settled) return;
12548
+ settled = true;
12549
+ clearTimeout(timer);
12550
+ resolve9(value);
12551
+ }, "finish");
12552
+ try {
12553
+ Promise.resolve(work()).then(finish, () => finish(void 0));
12554
+ } catch {
12555
+ finish(void 0);
12556
+ }
12557
+ });
12558
+ }
12559
+ __name(withDeadline, "withDeadline");
12560
+ function finiteOrNull(value) {
12561
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
12562
+ }
12563
+ __name(finiteOrNull, "finiteOrNull");
12564
+ function createHealthProbe(io, options) {
12565
+ const budget = options.timeoutMs ?? FLEET_HEALTH_PROBE_TIMEOUT_MS;
12566
+ const slowTtl = options.slowFactTtlMs ?? DEFAULT_SLOW_FACT_TTL_MS;
12567
+ const slowDeadline = options.slowFactDeadlineMs ?? DEFAULT_SLOW_FACT_DEADLINE_MS;
12568
+ const now = options.now ?? Date.now;
12569
+ const run = /* @__PURE__ */ __name(async (command, args, timeoutMs) => {
12570
+ try {
12571
+ return await io.runner.run(command, [...args], { timeoutMs });
12572
+ } catch {
12573
+ return null;
12574
+ }
12575
+ }, "run");
12576
+ const cpuAndMemory = /* @__PURE__ */ __name(async () => {
12577
+ const sample = await io.resourceProbe.sample();
12578
+ const out = {};
12579
+ const cpu = finiteOrNull(sample.cpuPercent);
12580
+ if (cpu !== null) out.cpuPercent = cpu;
12581
+ const total = finiteOrNull(sample.totalMemoryMb);
12582
+ const used = finiteOrNull(sample.usedMemoryMb);
12583
+ if (total !== null && total > 0) {
12584
+ out.memTotalMiB = Math.floor(total);
12585
+ if (used !== null && used >= 0) {
12586
+ out.memUsedMiB = Math.round(used);
12587
+ out.memPercent = used / total * 100;
12588
+ }
12589
+ }
12590
+ return out;
12591
+ }, "cpuAndMemory");
12592
+ const disk = /* @__PURE__ */ __name(async () => {
12593
+ const probe = io.disk;
12594
+ if (!probe?.volume) return {};
12595
+ const target = await nearestExistingPath(options.workspacePath);
12596
+ if (!target) return {};
12597
+ const reading = await probe.volume(target);
12598
+ if (!reading) return {};
12599
+ const used = Math.max(reading.totalBytes - reading.freeBytes, 0);
12600
+ const denominator = used + reading.availableBytes;
12601
+ const out = {
12602
+ diskFreeMiB: Math.floor(reading.availableBytes / BYTES_PER_MIB),
12603
+ diskTotalMiB: Math.floor(reading.totalBytes / BYTES_PER_MIB)
12604
+ };
12605
+ if (denominator > 0) out.diskPercent = used / denominator * 100;
12606
+ return out;
12607
+ }, "disk");
12608
+ const hostFacts = /* @__PURE__ */ __name(() => {
12609
+ const out = {};
12610
+ const cores = finiteOrNull(io.host.cores());
12611
+ if (cores !== null) out.cpuCores = cores;
12612
+ const uptime2 = finiteOrNull(io.host.uptimeSec());
12613
+ if (uptime2 !== null) out.uptimeSec = uptime2;
12614
+ if (io.platform !== "win32") {
12615
+ const load = finiteOrNull(io.host.loadAvg1());
12616
+ if (load !== null) out.loadAvg1 = load;
12617
+ }
12618
+ return out;
12619
+ }, "hostFacts");
12620
+ const windowsFacts = refreshInBackground(
12621
+ async () => {
12622
+ const result = await run(
12623
+ "powershell",
12624
+ ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodePowerShell(WINDOWS_HEALTH_SCRIPT)],
12625
+ slowDeadline
12626
+ );
12627
+ return result && result.code === 0 && !result.timedOut ? parseWindowsHealth(result.stdout) : null;
12628
+ },
12629
+ slowTtl,
12630
+ now
12631
+ );
12632
+ const linuxPendingReboot = refreshInBackground(
12633
+ async () => {
12634
+ if (await io.pathExists(LINUX_REBOOT_REQUIRED_PATH)) return true;
12635
+ if (await io.pathExists(LINUX_DEBIAN_RELEASE_PATH)) return false;
12636
+ const result = await run("needs-restarting", ["-r"], slowDeadline);
12637
+ if (!result || result.timedOut || result.spawnError) return null;
12638
+ if (result.code === 0) return false;
12639
+ if (result.code === 1 && NEEDS_RESTARTING_REBOOT_PATTERN.test(result.stdout)) return true;
12640
+ return null;
12641
+ },
12642
+ slowTtl,
12643
+ now
12644
+ );
12645
+ const linuxFailedServices = refreshInBackground(
12646
+ async () => {
12647
+ const result = await run(
12648
+ "systemctl",
12649
+ ["list-units", "--state=failed", "--no-legend", "--plain", "--no-pager"],
12650
+ slowDeadline
12651
+ );
12652
+ return result && result.code === 0 && !result.timedOut ? countSystemdFailedUnits(result.stdout) : null;
12653
+ },
12654
+ slowTtl,
12655
+ now
12656
+ );
12657
+ const gpuIdentity = refreshInBackground(
12658
+ () => detectGpuWithEvidence(io.runner, io.platform),
12659
+ GPU_IDENTITY_TTL_MS,
12660
+ now
12661
+ );
12662
+ const pendingRebootAndServices = /* @__PURE__ */ __name(async () => {
12663
+ const out = {};
12664
+ if (io.platform === "win32") {
12665
+ const facts = await windowsFacts();
12666
+ if (facts?.pendingReboot !== void 0) out.pendingReboot = facts.pendingReboot;
12667
+ if (facts?.failedServices !== void 0) out.failedServices = facts.failedServices;
12668
+ return out;
12669
+ }
12670
+ if (io.platform === "linux") {
12671
+ const [reboot, failed] = await Promise.all([
12672
+ withDeadline(linuxPendingReboot, budget),
12673
+ withDeadline(linuxFailedServices, budget)
12674
+ ]);
12675
+ if (typeof reboot === "boolean") out.pendingReboot = reboot;
12676
+ if (typeof failed === "number") out.failedServices = failed;
12677
+ }
12678
+ return out;
12679
+ }, "pendingRebootAndServices");
12680
+ const gpu = /* @__PURE__ */ __name(async () => {
12681
+ const nvidia = await run("nvidia-smi", NVIDIA_SMI_HEALTH_ARGS, budget);
12682
+ if (nvidia && nvidia.code === 0 && !nvidia.timedOut) {
12683
+ const parsed = parseNvidiaSmiHealth(nvidia.stdout);
12684
+ if (parsed.length > 0) return { gpu: parsed.map((gpu2) => ({ vendor: "nvidia", ...gpu2 })) };
12685
+ }
12686
+ if (io.platform === "win32") {
12687
+ const facts = await windowsFacts();
12688
+ if (!facts || facts.adapters === void 0) return {};
12689
+ if (facts.adapters.length === 0) return { gpu: null };
12690
+ return { gpu: facts.adapters.map((name) => ({ vendor: classifyGpuVendor(name), name })) };
12691
+ }
12692
+ const identity = await gpuIdentity();
12693
+ if (!identity) return {};
12694
+ if (!identity.gpu) return identity.answered ? { gpu: null } : {};
12695
+ const found = { vendor: identity.gpu.vendor, name: identity.gpu.model };
12696
+ if (identity.gpu.vendor === "amd" && io.platform === "linux") {
12697
+ const rocm = await run("rocm-smi", ROCM_SMI_HEALTH_ARGS, budget);
12698
+ const usage = rocm && rocm.code === 0 && !rocm.timedOut ? parseRocmSmiHealth(rocm.stdout) : null;
12699
+ if (usage) Object.assign(found, usage);
12700
+ }
12701
+ if (identity.gpu.vendor === "apple" && io.platform === "darwin") {
12702
+ const ioreg = await run("ioreg", IOREG_GPU_ARGS, budget);
12703
+ const util = ioreg && ioreg.code === 0 && !ioreg.timedOut ? parseIoregGpuUtil(ioreg.stdout) : null;
12704
+ if (util !== null) found.utilPercent = util;
12705
+ }
12706
+ return { gpu: [found] };
12707
+ }, "gpu");
12708
+ return async () => {
12709
+ try {
12710
+ const parts = await Promise.all([
12711
+ withDeadline(cpuAndMemory, budget),
12712
+ withDeadline(disk, budget),
12713
+ withDeadline(hostFacts, budget),
12714
+ withDeadline(pendingRebootAndServices, budget),
12715
+ withDeadline(gpu, budget)
12716
+ ]);
12717
+ const merged = {};
12718
+ for (const part of parts) {
12719
+ if (part) Object.assign(merged, part);
12720
+ }
12721
+ return sanitizeFleetNodeHealthSnapshot(merged);
12722
+ } catch {
12723
+ return null;
12724
+ }
12725
+ };
12726
+ }
12727
+ __name(createHealthProbe, "createHealthProbe");
12728
+
12729
+ // src/core/housekeeping-report.ts
12730
+ function summarizeWorkspaceReap(result) {
12731
+ const dryRun = result.dryRun === true;
12732
+ const worktrees = dryRun ? result.kept.length + result.removed.length : result.kept.length;
12733
+ const keptBytes = result.kept.reduce((sum, verdict) => sum + safeBytes(verdict.record.sizeBytes), 0);
12734
+ const keptPoolBytes = result.keptPools.reduce((sum, verdict) => sum + safeBytes(verdict.pool.sizeBytes), 0);
12735
+ const plannedBytes = dryRun ? result.removed.reduce((sum, entry) => sum + safeBytes(entry.freedBytes), 0) + result.removedPools.reduce((sum, entry) => sum + safeBytes(entry.freedBytes), 0) : 0;
12736
+ return {
12737
+ workspaceCount: worktrees,
12738
+ workspaceBytes: keptBytes + keptPoolBytes + plannedBytes,
12739
+ freedBytes: dryRun ? 0 : safeBytes(result.freedBytes),
12740
+ dryRun
12741
+ };
11409
12742
  }
11410
12743
  __name(summarizeWorkspaceReap, "summarizeWorkspaceReap");
11411
12744
  function safeBytes(value) {
@@ -11552,6 +12885,50 @@ var import_fs2 = require("fs");
11552
12885
  var import_fs3 = require("fs");
11553
12886
  var import_os = require("os");
11554
12887
  var import_path2 = require("path");
12888
+
12889
+ // src/core/execution-policy.ts
12890
+ var NodeExecutionPolicyError = class extends Error {
12891
+ static {
12892
+ __name(this, "NodeExecutionPolicyError");
12893
+ }
12894
+ constructor(message) {
12895
+ super(message);
12896
+ this.name = "NodeExecutionPolicyError";
12897
+ }
12898
+ };
12899
+ var NODE_POLICY_COMMAND_HINT = "The machine owner can allow it on this PC with `ever-works-node policy <flag> on` (then restart the node; for the desktop app, run the command with EVER_WORKS_NODE_CONFIG set to the app\u2019s node-config.json); the platform cannot change this setting";
12900
+ function describePhase(name, count) {
12901
+ return count > 0 ? `${count} ${name}${count === 1 ? "" : "s"}` : null;
12902
+ }
12903
+ __name(describePhase, "describePhase");
12904
+ function enforceAgentTaskExecutionPolicy(input, policy) {
12905
+ if (!policy) return;
12906
+ if (input.execution?.skipPermissions === true && !policy.allowSkipPermissions) {
12907
+ throw new NodeExecutionPolicyError(
12908
+ `Refused by this node's local execution policy: the job asks to run ${String(input.execution.provider)} with its permission checks switched off (skip-permissions), which gives an untrusted prompt an unrestricted shell as the machine owner. ${NODE_POLICY_COMMAND_HINT} (flag: --allow-skip-permissions).`
12909
+ );
12910
+ }
12911
+ if (policy.allowShellCommands) return;
12912
+ const phases = [
12913
+ describePhase("setup command", input.setupCount),
12914
+ describePhase(input.execution ? "command step" : "command-template step", input.stepCount),
12915
+ describePhase("acceptance check", input.checkCount)
12916
+ ].filter((phase) => phase !== null);
12917
+ if (phases.length === 0) return;
12918
+ throw new NodeExecutionPolicyError(
12919
+ `Refused by this node's local execution policy: the job carries ${phases.join(", ")}. Those are shell commands the node itself would run with the machine owner\u2019s privileges, executing repository code` + (input.execution ? " \u2014 after the model step, code the model may have just written" : "") + `. ${NODE_POLICY_COMMAND_HINT} (flag: --allow-shell-commands), or run this Work with checks off.`
12920
+ );
12921
+ }
12922
+ __name(enforceAgentTaskExecutionPolicy, "enforceAgentTaskExecutionPolicy");
12923
+ function enforceAcceptanceChecksExecutionPolicy(checkCount, policy) {
12924
+ if (!policy || policy.allowShellCommands || checkCount === 0) return;
12925
+ throw new NodeExecutionPolicyError(
12926
+ `Refused by this node's local execution policy: the job carries ${describePhase("acceptance check", checkCount)}, shell commands the node would run with the machine owner\u2019s privileges. ${NODE_POLICY_COMMAND_HINT} (flag: --allow-shell-commands).`
12927
+ );
12928
+ }
12929
+ __name(enforceAcceptanceChecksExecutionPolicy, "enforceAcceptanceChecksExecutionPolicy");
12930
+
12931
+ // src/core/executors/acceptance-checks.ts
11555
12932
  var DEFAULT_CHECK_TIMEOUT_SEC = 600;
11556
12933
  var MAX_CHECK_TIMEOUT_SEC = 1800;
11557
12934
  var CHECK_LOG_TAIL_BYTES = 4096;
@@ -11598,6 +12975,7 @@ async function runAcceptanceChecksJob(job, io = {}, signal) {
11598
12975
  throwIfCommandAborted(signal);
11599
12976
  return { gateStatus: "none", results: [] };
11600
12977
  }
12978
+ enforceAcceptanceChecksExecutionPolicy(checks.length, io.executionPolicy);
11601
12979
  const results = [];
11602
12980
  for (const check of checks) {
11603
12981
  throwIfCommandAborted(signal);
@@ -13329,6 +14707,10 @@ async function runAgentTaskJob(job, io = {}, signal) {
13329
14707
  const checks = withRunEnvGrants(resolveAcceptanceChecks(payload), runEnvGrants);
13330
14708
  const grantedSteps = withRunEnvGrants(steps, runEnvGrants);
13331
14709
  const setup = withRunEnvGrants(normalizeAgentTaskSetup(payload.setup), runEnvGrants);
14710
+ enforceAgentTaskExecutionPolicy(
14711
+ { execution, setupCount: setup.length, stepCount: grantedSteps.length, checkCount: checks.length },
14712
+ io.executionPolicy
14713
+ );
13332
14714
  let workspaceResolution;
13333
14715
  try {
13334
14716
  workspaceResolution = await resolveAgentTaskWorkspace(taskId, payload, io, signal);
@@ -13730,6 +15112,7 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
13730
15112
  resultPath: (0, import_path4.join)(scratchDir, "model-output.json")
13731
15113
  };
13732
15114
  const mcpConfigPath = (0, import_path4.join)(scratchDir, "mcp.json");
15115
+ const settingsPath = (0, import_path4.join)(scratchDir, "claude-settings.json");
13733
15116
  const containment = await establishModelContainment(execution.provider, scratchDir, scratchFs, io);
13734
15117
  const bridge = await startBridge(jobId, bridgeSpec, mcpConfigPath, scratchFs, io);
13735
15118
  const recordEvidence = io.modelTranscript !== "off";
@@ -13745,7 +15128,12 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
13745
15128
  await scratchFs.writeFile(mcpConfigPath, MODEL_CLI_EMPTY_MCP_CONFIG);
13746
15129
  emptyMcpConfigPath = mcpConfigPath;
13747
15130
  }
13748
- const droppedFlags = await optionalFlagsToDrop(jobId, execution, executable, io);
15131
+ let claudeSettingsPath;
15132
+ if (execution.provider === "claude-code") {
15133
+ await scratchFs.writeFile(settingsPath, MODEL_CLI_FLEET_SETTINGS);
15134
+ claudeSettingsPath = settingsPath;
15135
+ }
15136
+ const { dropped: droppedFlags, restricted } = await probeModelCliFlags(jobId, execution, executable, io);
13749
15137
  const codexUserSettings = execution.provider === "codex" ? await readOwnerCodexSettings(
13750
15138
  jobId,
13751
15139
  io,
@@ -13763,6 +15151,8 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
13763
15151
  ...mounts && mounts.length > 0 ? { mounts } : {},
13764
15152
  ...bridge.cli ? { mcp: bridge.cli } : {},
13765
15153
  ...emptyMcpConfigPath ? { emptyMcpConfigPath } : {},
15154
+ ...claudeSettingsPath ? { settingsPath: claudeSettingsPath } : {},
15155
+ ...restricted ? { restricted: true } : {},
13766
15156
  ...codexUserSettings ? { codexUserSettings } : {},
13767
15157
  ...resumeSessionId ? { resumeSessionId } : {},
13768
15158
  ...io.platform ? { platform: io.platform } : {},
@@ -13770,6 +15160,7 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
13770
15160
  ...droppedFlags.length > 0 ? { omitFlags: droppedFlags } : {}
13771
15161
  });
13772
15162
  assertMcpIsolationInCommand({ command, execution });
15163
+ assertSettingsIsolationInCommand({ command, execution });
13773
15164
  assertMountGrantsInCommand({
13774
15165
  command,
13775
15166
  execution,
@@ -13848,14 +15239,21 @@ async function refuseUnderClaudeManagedMcpConfig(provider, io) {
13848
15239
  );
13849
15240
  }
13850
15241
  __name(refuseUnderClaudeManagedMcpConfig, "refuseUnderClaudeManagedMcpConfig");
13851
- async function optionalFlagsToDrop(jobId, execution, executable, io) {
13852
- if (!io.modelCliCompat) return [];
15242
+ async function probeModelCliFlags(jobId, execution, executable, io) {
15243
+ if (!io.modelCliCompat) return { dropped: [], restricted: false };
13853
15244
  let compat = null;
13854
15245
  try {
13855
15246
  compat = await io.modelCliCompat(execution.provider, executable);
13856
15247
  } catch {
13857
- return [];
15248
+ return { dropped: [], restricted: false };
13858
15249
  }
15250
+ return {
15251
+ dropped: optionalFlagsToDrop(jobId, execution, compat, io),
15252
+ restricted: supportsRestrictedMode(jobId, execution, compat, io)
15253
+ };
15254
+ }
15255
+ __name(probeModelCliFlags, "probeModelCliFlags");
15256
+ function optionalFlagsToDrop(jobId, execution, compat, io) {
13859
15257
  const dropped = unsupportedOptionalModelCliFlags(execution, compat?.supportedFlags ?? null);
13860
15258
  if (dropped.length > 0) {
13861
15259
  io.logger?.warn(
@@ -13865,6 +15263,18 @@ async function optionalFlagsToDrop(jobId, execution, executable, io) {
13865
15263
  return dropped;
13866
15264
  }
13867
15265
  __name(optionalFlagsToDrop, "optionalFlagsToDrop");
15266
+ function supportsRestrictedMode(jobId, execution, compat, io) {
15267
+ if (execution.provider !== "claude-code" || execution.skipPermissions === true) return false;
15268
+ if (io.executionPolicy?.restrictedMode === false) return false;
15269
+ const supported = compat?.supportedFlags?.has("--restricted") === true;
15270
+ if (!supported && compat?.supportedFlags) {
15271
+ io.logger?.info(
15272
+ `[fleet-node] job ${jobId}: the pinned claude-code CLI (${compat.version ?? "version unknown"}) does not advertise --restricted \u2014 running with the baseline settings isolation only. Upgrade Claude Code on this machine for the stricter mode.`
15273
+ );
15274
+ }
15275
+ return supported;
15276
+ }
15277
+ __name(supportsRestrictedMode, "supportsRestrictedMode");
13868
15278
  async function establishModelContainment(provider, scratchDir, scratchFs, io) {
13869
15279
  const downgrades = [
13870
15280
  {
@@ -15346,17 +16756,9 @@ function describe(error) {
15346
16756
  }
15347
16757
  __name(describe, "describe");
15348
16758
 
15349
- // src/core/screen/node-leg.ts
15350
- var NODE_LEG_RECONNECT_BASE_MS = 2e3;
15351
- var NODE_LEG_RECONNECT_MAX_MS = 3e4;
15352
- var NODE_LEG_ACCEPTED_AFTER_MS = 3e4;
16759
+ // src/core/link/link-socket.ts
15353
16760
  var LOOPBACK_HOSTNAMES2 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
15354
- function toComputerWsUrl(apiUrl, wsPath) {
15355
- const origin = new URL(apiUrl).origin;
15356
- return `${origin.replace(/^http/, "ws")}${wsPath.startsWith("/") ? wsPath : `/${wsPath}`}`;
15357
- }
15358
- __name(toComputerWsUrl, "toComputerWsUrl");
15359
- function isSecureLegOrigin(apiUrl) {
16761
+ function isSecureLinkOrigin(apiUrl) {
15360
16762
  let parsed;
15361
16763
  try {
15362
16764
  parsed = new URL(apiUrl);
@@ -15366,6 +16768,83 @@ function isSecureLegOrigin(apiUrl) {
15366
16768
  if (parsed.protocol === "https:") return true;
15367
16769
  return parsed.protocol === "http:" && LOOPBACK_HOSTNAMES2.has(parsed.hostname);
15368
16770
  }
16771
+ __name(isSecureLinkOrigin, "isSecureLinkOrigin");
16772
+ function toLinkWsUrl(apiUrl, wsPath) {
16773
+ const origin = new URL(apiUrl).origin;
16774
+ return `${origin.replace(/^http/, "ws")}${wsPath.startsWith("/") ? wsPath : `/${wsPath}`}`;
16775
+ }
16776
+ __name(toLinkWsUrl, "toLinkWsUrl");
16777
+ var LinkBackoff = class {
16778
+ static {
16779
+ __name(this, "LinkBackoff");
16780
+ }
16781
+ attempt = 0;
16782
+ baseMs;
16783
+ maxMs;
16784
+ jitter;
16785
+ random;
16786
+ constructor(options = {}) {
16787
+ this.baseMs = options.baseMs ?? FLEET_LINK_BACKOFF_BASE_MS;
16788
+ this.maxMs = Math.max(this.baseMs, options.maxMs ?? FLEET_LINK_BACKOFF_MAX_MS);
16789
+ this.jitter = Math.min(Math.max(options.jitter ?? 0.2, 0), 0.9);
16790
+ this.random = options.random ?? Math.random;
16791
+ }
16792
+ /** Attempts made since the last reset. */
16793
+ get attempts() {
16794
+ return this.attempt;
16795
+ }
16796
+ /** The nominal (un-jittered) wait for attempt `n`. */
16797
+ nominal(attempt) {
16798
+ return Math.min(this.baseMs * 2 ** Math.min(Math.max(attempt, 0), 20), this.maxMs);
16799
+ }
16800
+ next() {
16801
+ const nominal = this.nominal(this.attempt);
16802
+ this.attempt += 1;
16803
+ const spread = nominal * this.jitter * (this.random() * 2 - 1);
16804
+ return Math.round(Math.min(Math.max(nominal + spread, this.baseMs), this.maxMs));
16805
+ }
16806
+ reset() {
16807
+ this.attempt = 0;
16808
+ }
16809
+ };
16810
+ var LINK_SOCKET_OPEN = 1;
16811
+ function defaultLinkSocketFactory() {
16812
+ const Ctor = globalThis.WebSocket;
16813
+ if (typeof Ctor !== "function") return null;
16814
+ return (url) => {
16815
+ const socket = new Ctor(url);
16816
+ try {
16817
+ socket.binaryType = "arraybuffer";
16818
+ } catch {
16819
+ }
16820
+ return socket;
16821
+ };
16822
+ }
16823
+ __name(defaultLinkSocketFactory, "defaultLinkSocketFactory");
16824
+ function linkMessageBytes(data) {
16825
+ if (data instanceof Uint8Array) return data;
16826
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
16827
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
16828
+ return null;
16829
+ }
16830
+ __name(linkMessageBytes, "linkMessageBytes");
16831
+ function linkCloseCode(event) {
16832
+ const code = event?.code;
16833
+ return typeof code === "number" ? code : null;
16834
+ }
16835
+ __name(linkCloseCode, "linkCloseCode");
16836
+
16837
+ // src/core/screen/node-leg.ts
16838
+ var NODE_LEG_RECONNECT_BASE_MS = 2e3;
16839
+ var NODE_LEG_RECONNECT_MAX_MS = 3e4;
16840
+ var NODE_LEG_ACCEPTED_AFTER_MS = 3e4;
16841
+ function toComputerWsUrl(apiUrl, wsPath) {
16842
+ return toLinkWsUrl(apiUrl, wsPath);
16843
+ }
16844
+ __name(toComputerWsUrl, "toComputerWsUrl");
16845
+ function isSecureLegOrigin(apiUrl) {
16846
+ return isSecureLinkOrigin(apiUrl);
16847
+ }
15369
16848
  __name(isSecureLegOrigin, "isSecureLegOrigin");
15370
16849
  function openNodeLeg(options) {
15371
16850
  if (!isSecureLegOrigin(options.apiUrl)) {
@@ -15534,13 +17013,13 @@ var ComputerSessionPayloadError = class extends Error {
15534
17013
  this.name = "ComputerSessionPayloadError";
15535
17014
  }
15536
17015
  };
15537
- var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17016
+ var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15538
17017
  function normalizeComputerSessionPayload(raw, nodeId) {
15539
17018
  if (!raw || typeof raw !== "object") throw new ComputerSessionPayloadError("Live view payload is missing");
15540
17019
  const payload = raw;
15541
17020
  const sessionId = typeof payload.sessionId === "string" ? payload.sessionId : "";
15542
17021
  const agentId = typeof payload.agentId === "string" ? payload.agentId : "";
15543
- if (!UUID_PATTERN.test(sessionId) || !UUID_PATTERN.test(agentId)) {
17022
+ if (!UUID_PATTERN2.test(sessionId) || !UUID_PATTERN2.test(agentId)) {
15544
17023
  throw new ComputerSessionPayloadError("Live view payload has no valid session or Agent");
15545
17024
  }
15546
17025
  if (payload.nodeId !== nodeId) {
@@ -15797,6 +17276,7 @@ var PROBLEM_CODES2 = [
15797
17276
  "already_connected",
15798
17277
  "already_linked",
15799
17278
  "already_claimed",
17279
+ "claim_locked",
15800
17280
  "invalid_client",
15801
17281
  "public_jwk_invalid",
15802
17282
  "identifier_claimed",
@@ -15822,6 +17302,11 @@ var PROBLEM_CODES2 = [
15822
17302
  "database_absent",
15823
17303
  "database_unreachable",
15824
17304
  "topic_private",
17305
+ "realtime_disabled",
17306
+ "topic_forbidden",
17307
+ "topic_invalid",
17308
+ "realtime_busy",
17309
+ "too_many_streams",
15825
17310
  "keys_unavailable",
15826
17311
  "internal_error"
15827
17312
  ];
@@ -16406,6 +17891,49 @@ var FLEET_AGENT_CREDENTIAL_FAMILIES2 = [
16406
17891
  FLEET_AGENT_CREDENTIAL_FAMILIES2.flatMap(
16407
17892
  (family) => family.envNames
16408
17893
  );
17894
+ var FLEET_LINK_FRAME_KINDS2 = {
17895
+ /** Raw bytes: terminal output/input, log lines, file chunks. */
17896
+ DATA: 1,
17897
+ /** `[cols u16][rows u16]`. */
17898
+ RESIZE: 2,
17899
+ /** `[code i32]`. */
17900
+ EXIT: 3,
17901
+ /** UTF-8 JSON control message. */
17902
+ JSON: 10,
17903
+ /** UTF-8 reason code. */
17904
+ CLOSE: 11,
17905
+ /** A full picture (the existing computer-frame encoding). */
17906
+ FRAME: 20,
17907
+ /** A pointer / key / text event (the existing computer input encoding). */
17908
+ INPUT: 21,
17909
+ /** Changed tiles `[w][h][count]{x,y,w,h,len,jpeg}`. */
17910
+ TILES: 23,
17911
+ /** `[bytes u32]` credit. */
17912
+ ACK: 30,
17913
+ /** End of a stream. */
17914
+ END: 31,
17915
+ /** UTF-8 text ≤ 256 KiB, pasted into a desktop. */
17916
+ CLIP: 40
17917
+ };
17918
+ new Set(Object.values(FLEET_LINK_FRAME_KINDS2));
17919
+ var FLEET_LINK_CONTROL_TYPES2 = [
17920
+ "link.hello",
17921
+ "link.ready",
17922
+ "link.ping",
17923
+ "link.pong",
17924
+ "link.pause",
17925
+ "link.resume",
17926
+ "link.drain",
17927
+ "channel.open",
17928
+ "channel.ready",
17929
+ "channel.refused",
17930
+ "channel.close",
17931
+ "operation.progress",
17932
+ "operation.done",
17933
+ "operation.cancel"
17934
+ ];
17935
+ new Set(FLEET_LINK_CONTROL_TYPES2);
17936
+ new TextEncoder();
16409
17937
  var EMAIL_INBOX_CAP_FIELDS2 = [
16410
17938
  "inboxDailySends",
16411
17939
  "inboxBurstSends",
@@ -20148,6 +21676,7 @@ var PROBLEM_CODES3 = [
20148
21676
  "already_connected",
20149
21677
  "already_linked",
20150
21678
  "already_claimed",
21679
+ "claim_locked",
20151
21680
  "invalid_client",
20152
21681
  "public_jwk_invalid",
20153
21682
  "identifier_claimed",
@@ -20173,6 +21702,11 @@ var PROBLEM_CODES3 = [
20173
21702
  "database_absent",
20174
21703
  "database_unreachable",
20175
21704
  "topic_private",
21705
+ "realtime_disabled",
21706
+ "topic_forbidden",
21707
+ "topic_invalid",
21708
+ "realtime_busy",
21709
+ "too_many_streams",
20176
21710
  "keys_unavailable",
20177
21711
  "internal_error"
20178
21712
  ];
@@ -20757,6 +22291,49 @@ var FLEET_AGENT_CREDENTIAL_FAMILIES3 = [
20757
22291
  FLEET_AGENT_CREDENTIAL_FAMILIES3.flatMap(
20758
22292
  (family) => family.envNames
20759
22293
  );
22294
+ var FLEET_LINK_FRAME_KINDS3 = {
22295
+ /** Raw bytes: terminal output/input, log lines, file chunks. */
22296
+ DATA: 1,
22297
+ /** `[cols u16][rows u16]`. */
22298
+ RESIZE: 2,
22299
+ /** `[code i32]`. */
22300
+ EXIT: 3,
22301
+ /** UTF-8 JSON control message. */
22302
+ JSON: 10,
22303
+ /** UTF-8 reason code. */
22304
+ CLOSE: 11,
22305
+ /** A full picture (the existing computer-frame encoding). */
22306
+ FRAME: 20,
22307
+ /** A pointer / key / text event (the existing computer input encoding). */
22308
+ INPUT: 21,
22309
+ /** Changed tiles `[w][h][count]{x,y,w,h,len,jpeg}`. */
22310
+ TILES: 23,
22311
+ /** `[bytes u32]` credit. */
22312
+ ACK: 30,
22313
+ /** End of a stream. */
22314
+ END: 31,
22315
+ /** UTF-8 text ≤ 256 KiB, pasted into a desktop. */
22316
+ CLIP: 40
22317
+ };
22318
+ new Set(Object.values(FLEET_LINK_FRAME_KINDS3));
22319
+ var FLEET_LINK_CONTROL_TYPES3 = [
22320
+ "link.hello",
22321
+ "link.ready",
22322
+ "link.ping",
22323
+ "link.pong",
22324
+ "link.pause",
22325
+ "link.resume",
22326
+ "link.drain",
22327
+ "channel.open",
22328
+ "channel.ready",
22329
+ "channel.refused",
22330
+ "channel.close",
22331
+ "operation.progress",
22332
+ "operation.done",
22333
+ "operation.cancel"
22334
+ ];
22335
+ new Set(FLEET_LINK_CONTROL_TYPES3);
22336
+ new TextEncoder();
20760
22337
  var EMAIL_INBOX_CAP_FIELDS3 = [
20761
22338
  "inboxDailySends",
20762
22339
  "inboxBurstSends",
@@ -22051,119 +23628,1230 @@ async function defaultReadTextFile(path2) {
22051
23628
  }
22052
23629
  __name(defaultReadTextFile, "defaultReadTextFile");
22053
23630
 
22054
- // src/core/runtime.ts
22055
- function buildSelfDescriptionTelemetry(io, cliCompat) {
22056
- const pinned = /* @__PURE__ */ __name(() => io.environment.modelCli ?? {}, "pinned");
22057
- const telemetry = {
22058
- cliVersion: cliCompat ? async () => primaryPinnedCliVersion(await cliCompat.probeAll(pinned())) ?? detectAgentCliVersion(io.runner) : () => detectAgentCliVersion(io.runner),
22059
- modelIdentity: cacheProbe(() => detectModelIdentity(io.runner, io.environment.modelCli ?? {}))
22060
- };
22061
- if (cliCompat) {
22062
- telemetry.cliVersions = async () => describeCliVersions(await cliCompat.probeAll(pinned()));
22063
- }
22064
- if (io.diskProbe) {
22065
- const probe = io.diskProbe;
22066
- const path2 = io.workspacePath ?? process.cwd();
22067
- telemetry.diskFreeBytes = () => measureWorkspaceFreeBytes(probe, path2);
22068
- }
22069
- return telemetry;
22070
- }
22071
- __name(buildSelfDescriptionTelemetry, "buildSelfDescriptionTelemetry");
22072
- function clampHeartbeatInterval(intervalMs) {
22073
- if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs)) {
22074
- return DEFAULT_HEARTBEAT_INTERVAL_MS;
22075
- }
22076
- return Math.min(Math.max(Math.round(intervalMs), MIN_HEARTBEAT_INTERVAL_MS), MAX_HEARTBEAT_INTERVAL_MS);
22077
- }
22078
- __name(clampHeartbeatInterval, "clampHeartbeatInterval");
22079
- async function enrollNode(options) {
22080
- const { logger } = options;
22081
- logger.protect(options.token);
22082
- const client = new FleetClient({
22083
- apiUrl: options.apiUrl,
22084
- fetchFn: options.fetchFn,
22085
- logger,
22086
- userAgent: options.userAgent ?? `ever-works-node/${options.version}`
22087
- });
22088
- const description = await describeSelf(
22089
- options.runner,
22090
- options.environment,
22091
- options.version,
22092
- options.capabilitySelection ?? null,
22093
- // Deliberately WITHOUT the pinned-CLI compat probe (review, slice AR):
22094
- // that probe adds `cliVersions`, an optional field an older platform's
22095
- // `forbidNonWhitelisted` pipe answers with a 400 — and enrollment, unlike
22096
- // the heartbeat, has no strip-and-retry fallback, so a new node could
22097
- // never enroll against it. The first heartbeat reports the pinned
22098
- // versions, behind the heartbeat's own fallback.
22099
- buildSelfDescriptionTelemetry(options)
22100
- );
22101
- logger.info(`Enrolling with ${client.baseUrl} as ${description.platform} [${description.capabilities.join(", ")}]`);
22102
- const result = await client.enroll({ token: options.token, ...description });
22103
- logger.protect(result.secret);
22104
- const limits = clampResourceLimits(options.limits);
22105
- const config = {
22106
- apiUrl: client.baseUrl,
22107
- nodeId: result.nodeId,
22108
- secret: result.secret,
22109
- kind: options.kind,
22110
- capabilities: description.capabilities,
22111
- limits,
22112
- heartbeatIntervalMs: clampHeartbeatInterval(options.heartbeatIntervalMs),
22113
- enrolledAt: new Date(options.now ? options.now() : Date.now()).toISOString()
22114
- };
22115
- if (options.capabilitySelection) {
22116
- config.capabilitySelection = [...options.capabilitySelection];
23631
+ // src/core/link/link-client.ts
23632
+ var import_node_crypto3 = require("node:crypto");
23633
+
23634
+ // src/core/link/channel-mux.ts
23635
+ var ChannelMux = class {
23636
+ constructor(options) {
23637
+ this.options = options;
22117
23638
  }
22118
- const label = options.name?.trim() || result.node.name;
22119
- if (label) {
22120
- config.name = label;
23639
+ options;
23640
+ static {
23641
+ __name(this, "ChannelMux");
23642
+ }
23643
+ channels = /* @__PURE__ */ new Map();
23644
+ /** Numbers closed on the current generation. */
23645
+ retired = /* @__PURE__ */ new Set();
23646
+ sender = null;
23647
+ /** Channels currently open (suspended ones included). */
23648
+ get size() {
23649
+ return this.channels.size;
23650
+ }
23651
+ /** Point the table at a socket (null while the link is down). */
23652
+ bind(sender) {
23653
+ this.sender = sender;
23654
+ }
23655
+ has(channel) {
23656
+ return this.channels.has(channel);
23657
+ }
23658
+ /** Send a raw frame through the bound socket. */
23659
+ sendFrame(frame) {
23660
+ if (!frame || !this.sender) return false;
23661
+ try {
23662
+ return this.sender(frame);
23663
+ } catch {
23664
+ return false;
23665
+ }
22121
23666
  }
22122
- logger.info(
22123
- `Enrolled as node ${result.nodeId} ("${config.name ?? "unnamed"}") \u2014 max ${limits.maxConcurrentJobs} concurrent job(s)`
22124
- );
22125
- return config;
22126
- }
22127
- __name(enrollNode, "enrollNode");
22128
- function createNodeRuntime(config, io, options = {}) {
22129
- io.logger.protect(config.secret);
22130
- const userAgent = io.userAgent ?? `ever-works-node/${io.version}`;
22131
- const apiBase = resolveApiBase(config, options.env ?? process.env);
22132
- io.logger.info(`Control plane: ${describeApiBase(apiBase)}`);
22133
- if (apiBase.mismatch) {
22134
- io.logger.warn(
22135
- `The pinned control plane (${apiBase.url}) is not the origin this node enrolled against (${apiBase.configuredUrl}). Every call will be refused with 401 until the pin is corrected or the node is re-enrolled.`
22136
- );
23667
+ sendControl(message) {
23668
+ return this.sendFrame(encodeFleetLinkControl(message));
22137
23669
  }
22138
- const client = new FleetClient({
22139
- apiUrl: apiBase.url,
22140
- fetchFn: io.fetchFn,
22141
- logger: io.logger,
22142
- userAgent
22143
- });
22144
- const selection = config.capabilitySelection ?? null;
22145
- const workspaceRoot = options.agentTaskWorkspaceRoot ?? defaultFleetTaskWorkspaceRoot();
22146
- const cliCompat = new ModelCliCompatibilityProbe({
22147
- runner: io.runner,
22148
- platform: io.environment.platform,
22149
- ...io.statFile ? { statFile: io.statFile } : {},
22150
- ...io.now ? { now: io.now } : {}
22151
- });
22152
- const pinnedModelCli = options.modelCli ?? io.environment.modelCli ?? {};
22153
- const telemetry = buildSelfDescriptionTelemetry(
22154
- {
22155
- ...io,
22156
- workspacePath: io.workspacePath ?? workspaceRoot,
23670
+ /** `channel.open` from the platform: answer `channel.ready` or `channel.refused`. */
23671
+ async open(message) {
23672
+ const { channel, family, params, requestId } = message;
23673
+ const refuse = /* @__PURE__ */ __name((error) => {
23674
+ this.sendControl({ type: "channel.refused", channel, requestId, error });
23675
+ }, "refuse");
23676
+ if (this.channels.has(channel) || this.retired.has(channel)) {
23677
+ refuse(makeFleetLinkError("invalid", `channel ${channel} is already in use on this link`));
23678
+ return;
23679
+ }
23680
+ if (this.channels.size >= FLEET_LINK_MAX_CHANNELS) {
23681
+ refuse(
23682
+ makeFleetLinkError(
23683
+ "limit_exceeded",
23684
+ `${FLEET_LINK_MAX_CHANNELS} channels are open on ${this.options.nodeName}; close one`
23685
+ )
23686
+ );
23687
+ return;
23688
+ }
23689
+ if (this.options.control.paused) {
23690
+ refuse(makeFleetLinkError("kill_switch", "clear it under Settings \u2192 Fleet"));
23691
+ return;
23692
+ }
23693
+ const provider = this.options.modules.family(family);
23694
+ if (!provider) {
23695
+ refuse(
23696
+ makeFleetLinkError(
23697
+ "unsupported_on_node",
23698
+ `${this.options.nodeName} (node ${this.options.nodeVersion}) cannot do this yet: ${family}`,
23699
+ { family, nodeVersion: this.options.nodeVersion }
23700
+ )
23701
+ );
23702
+ return;
23703
+ }
23704
+ let requirement;
23705
+ try {
23706
+ requirement = this.options.modules.familyConsent(family, params);
23707
+ } catch (thrown) {
23708
+ this.options.logger?.warn(
23709
+ `Link channel ${channel} (${family}) consent check failed: ${thrown instanceof Error ? thrown.message : String(thrown)}`
23710
+ );
23711
+ refuse(makeFleetLinkError("internal", `${this.options.nodeName} could not check this channel's consent`));
23712
+ return;
23713
+ }
23714
+ if (!fleetConsentSatisfies(this.options.consents, requirement)) {
23715
+ refuse(
23716
+ makeFleetLinkError("consent_missing", fleetConsentMissingHint(requirement, this.options.nodeName), {
23717
+ family,
23718
+ consent: requirement
23719
+ })
23720
+ );
23721
+ return;
23722
+ }
23723
+ const placeholder = { channel, family, handler: {}, credit: null, suspended: false };
23724
+ this.channels.set(channel, placeholder);
23725
+ let result;
23726
+ try {
23727
+ result = await provider.open(this.contextFor(channel, family, params));
23728
+ } catch (thrown) {
23729
+ result = {
23730
+ ok: false,
23731
+ error: makeFleetLinkError(
23732
+ "operation_failed",
23733
+ (thrown instanceof Error ? thrown.message : String(thrown)).split(/\r?\n/, 1)[0].slice(0, 300)
23734
+ )
23735
+ };
23736
+ }
23737
+ if (this.channels.get(channel) !== placeholder) {
23738
+ if (result.ok) result.handler.onClose?.("normal");
23739
+ return;
23740
+ }
23741
+ if (!result.ok) {
23742
+ this.channels.delete(channel);
23743
+ this.retired.add(channel);
23744
+ refuse(result.error);
23745
+ return;
23746
+ }
23747
+ placeholder.handler = result.handler;
23748
+ placeholder.credit = FLEET_LINK_CREDIT_FAMILIES.includes(family) ? FLEET_LINK_CREDIT_WINDOW_BYTES : null;
23749
+ const ready = {
23750
+ type: "channel.ready",
23751
+ channel,
23752
+ requestId
23753
+ };
23754
+ if (result.handler.resumeKey) ready.resumeKey = result.handler.resumeKey;
23755
+ this.sendControl(ready);
23756
+ }
23757
+ /** A frame for a channel above 0. Frames for an unknown channel are dropped. */
23758
+ deliver(frame) {
23759
+ const entry = this.channels.get(frame.channel);
23760
+ if (!entry || entry.suspended) return;
23761
+ if (frame.kind === FLEET_LINK_FRAME_KINDS.ACK && entry.credit !== null) {
23762
+ const granted = decodeFleetLinkAck(frame.payload);
23763
+ if (granted !== null && granted > 0) {
23764
+ entry.credit = Math.min(entry.credit + granted, FLEET_LINK_CREDIT_WINDOW_BYTES);
23765
+ try {
23766
+ entry.handler.onDrain?.();
23767
+ } catch {
23768
+ }
23769
+ }
23770
+ return;
23771
+ }
23772
+ try {
23773
+ if (frame.kind === FLEET_LINK_FRAME_KINDS.JSON) {
23774
+ const message = decodeFleetLinkJson(frame.payload, FLEET_LINK_MAX_PAYLOAD_BYTES);
23775
+ if (message) entry.handler.onJson?.(message);
23776
+ return;
23777
+ }
23778
+ entry.handler.onBinary?.(frame.kind, frame.payload);
23779
+ } catch (thrown) {
23780
+ this.options.logger?.warn(
23781
+ `Link channel ${frame.channel} (${entry.family}) handler failed: ${thrown instanceof Error ? thrown.message : String(thrown)}`
23782
+ );
23783
+ }
23784
+ }
23785
+ /** `channel.close` from the platform. */
23786
+ closeRemote(channel, reason) {
23787
+ this.finish(channel, reason, false);
23788
+ }
23789
+ /** Close one channel from the node's side, telling the platform. */
23790
+ closeLocal(channel, reason, hint) {
23791
+ if (!this.channels.has(channel)) return;
23792
+ this.finish(channel, reason, true, hint);
23793
+ }
23794
+ /**
23795
+ * The link dropped. Channels that cannot resume are closed now with
23796
+ * `resume_failed`; the rest are suspended and listed for the reconnect.
23797
+ */
23798
+ suspend() {
23799
+ this.sender = null;
23800
+ const resume = [];
23801
+ for (const entry of [...this.channels.values()]) {
23802
+ if (entry.handler.resumeKey) {
23803
+ entry.suspended = true;
23804
+ resume.push({ channel: entry.channel, family: entry.family, resumeKey: entry.handler.resumeKey });
23805
+ } else {
23806
+ this.finish(entry.channel, "resume_failed", false);
23807
+ }
23808
+ }
23809
+ return resume;
23810
+ }
23811
+ /**
23812
+ * A new generation is ready. Channels it `resumed` carry on; every other
23813
+ * suspended channel closes with the reason the platform gave, or
23814
+ * `resume_failed`. Numbers retired on the previous generation are free
23815
+ * again — retirement is per generation.
23816
+ */
23817
+ resumeGeneration(resumed, closed, closedReason = "resume_failed") {
23818
+ this.retired = /* @__PURE__ */ new Set();
23819
+ const keep = new Set(resumed);
23820
+ for (const entry of [...this.channels.values()]) {
23821
+ if (!entry.suspended) continue;
23822
+ if (keep.has(entry.channel)) {
23823
+ entry.suspended = false;
23824
+ try {
23825
+ entry.handler.onResume?.();
23826
+ } catch {
23827
+ }
23828
+ } else {
23829
+ this.finish(entry.channel, closed.includes(entry.channel) ? closedReason : "resume_failed", false);
23830
+ }
23831
+ }
23832
+ }
23833
+ /** Close every channel (shutdown, or a link that will not come back). */
23834
+ closeAll(reason) {
23835
+ for (const channel of [...this.channels.keys()]) this.finish(channel, reason, false);
23836
+ }
23837
+ finish(channel, reason, tellPlatform, hint) {
23838
+ const entry = this.channels.get(channel);
23839
+ if (!entry) return;
23840
+ this.channels.delete(channel);
23841
+ this.retired.add(channel);
23842
+ if (tellPlatform) {
23843
+ const close = {
23844
+ type: "channel.close",
23845
+ channel,
23846
+ reason
23847
+ };
23848
+ if (hint) close.hint = hint;
23849
+ this.sendControl(close);
23850
+ }
23851
+ try {
23852
+ entry.handler.onClose?.(reason);
23853
+ } catch {
23854
+ }
23855
+ }
23856
+ contextFor(channel, family, params) {
23857
+ const live = /* @__PURE__ */ __name(() => {
23858
+ const entry = this.channels.get(channel);
23859
+ return entry && !entry.suspended ? entry : null;
23860
+ }, "live");
23861
+ return {
23862
+ channel,
23863
+ family,
23864
+ params,
23865
+ consents: this.options.consents,
23866
+ control: this.options.control,
23867
+ nodeVersion: this.options.nodeVersion,
23868
+ nodeName: this.options.nodeName,
23869
+ logger: this.options.logger,
23870
+ operations: this.options.operations,
23871
+ sendJson: /* @__PURE__ */ __name((message) => live() ? this.sendFrame(encodeFleetLinkJson(channel, message)) : false, "sendJson"),
23872
+ sendBinary: /* @__PURE__ */ __name((kind, payload) => {
23873
+ const entry = live();
23874
+ if (!entry) return false;
23875
+ if (kind === FLEET_LINK_FRAME_KINDS.DATA && entry.credit !== null) {
23876
+ if (payload.byteLength > entry.credit) return false;
23877
+ const sent = this.sendFrame(encodeFleetLinkFrame(channel, kind, payload));
23878
+ if (sent) entry.credit -= payload.byteLength;
23879
+ return sent;
23880
+ }
23881
+ return this.sendFrame(encodeFleetLinkFrame(channel, kind, payload));
23882
+ }, "sendBinary"),
23883
+ sendControl: /* @__PURE__ */ __name((message) => this.sendControl(message), "sendControl"),
23884
+ close: /* @__PURE__ */ __name((reason, hint) => this.closeLocal(channel, reason, hint), "close")
23885
+ };
23886
+ }
23887
+ };
23888
+
23889
+ // src/core/link/modules/registry.ts
23890
+ var MachineVerbError = class extends Error {
23891
+ constructor(error) {
23892
+ super(error.hint);
23893
+ this.error = error;
23894
+ this.name = "MachineVerbError";
23895
+ }
23896
+ error;
23897
+ static {
23898
+ __name(this, "MachineVerbError");
23899
+ }
23900
+ };
23901
+ var MODULE_ID_PATTERN = /^[a-z][a-z0-9-]{0,47}$/;
23902
+ function composeNodeModules(modules) {
23903
+ const ids = /* @__PURE__ */ new Set();
23904
+ const families = /* @__PURE__ */ new Map();
23905
+ const verbs = /* @__PURE__ */ new Map();
23906
+ const controls = /* @__PURE__ */ new Map();
23907
+ for (const module2 of modules) {
23908
+ if (!MODULE_ID_PATTERN.test(module2.id)) throw new Error(`Node module id "${module2.id}" is not kebab-case`);
23909
+ if (ids.has(module2.id)) throw new Error(`Node module "${module2.id}" is registered twice`);
23910
+ ids.add(module2.id);
23911
+ for (const provider of module2.families ?? []) {
23912
+ if (families.has(provider.family)) {
23913
+ throw new Error(
23914
+ `Channel family "${provider.family}" is claimed by two node modules (second: ${module2.id})`
23915
+ );
23916
+ }
23917
+ families.set(provider.family, provider);
23918
+ }
23919
+ for (const [name, verb] of Object.entries(module2.machineVerbs ?? {})) {
23920
+ if (!isFleetLinkVerb(name))
23921
+ throw new Error(`Machine verb "${name}" (module ${module2.id}) is not a valid verb name`);
23922
+ if (verbs.has(name))
23923
+ throw new Error(`Machine verb "${name}" is registered by two node modules (second: ${module2.id})`);
23924
+ verbs.set(name, verb);
23925
+ }
23926
+ for (const [type, handler] of Object.entries(module2.controlHandlers ?? {})) {
23927
+ if (controls.has(type))
23928
+ throw new Error(`Control type "${type}" is handled by two node modules (second: ${module2.id})`);
23929
+ controls.set(type, handler);
23930
+ }
23931
+ }
23932
+ const frozen = Object.freeze([...modules]);
23933
+ return {
23934
+ modules: frozen,
23935
+ tags(environment) {
23936
+ const out = [];
23937
+ for (const module2 of frozen) {
23938
+ for (const tag of module2.tags?.(environment) ?? []) {
23939
+ if (typeof tag === "string" && tag.length > 0 && !out.includes(tag)) out.push(tag);
23940
+ }
23941
+ }
23942
+ return out;
23943
+ },
23944
+ family: /* @__PURE__ */ __name((name) => families.get(name), "family"),
23945
+ familyConsent(name, params) {
23946
+ const provider = families.get(name);
23947
+ const declared = provider?.consent;
23948
+ if (typeof declared === "function") return declared(params);
23949
+ return declared ?? fleetLinkChannelOpenConsent(name);
23950
+ },
23951
+ machineVerb: /* @__PURE__ */ __name((name) => verbs.get(name), "machineVerb"),
23952
+ machineVerbs: /* @__PURE__ */ __name(() => [...verbs.keys()].sort(), "machineVerbs"),
23953
+ controlHandler: /* @__PURE__ */ __name((type) => controls.get(type), "controlHandler")
23954
+ };
23955
+ }
23956
+ __name(composeNodeModules, "composeNodeModules");
23957
+
23958
+ // src/core/link/modules/machine.ts
23959
+ var MACHINE_CHANNEL_MAX_IN_FLIGHT = 32;
23960
+ function errorFrom(thrown, signal) {
23961
+ if (thrown instanceof MachineVerbError) return thrown.error;
23962
+ if (signal.aborted) return makeFleetLinkError("operation_failed", "cancelled");
23963
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
23964
+ const firstLine = message.split(/\r?\n/, 1)[0]?.trim() || "the request failed on the node";
23965
+ return makeFleetLinkError("operation_failed", firstLine.slice(0, 300));
23966
+ }
23967
+ __name(errorFrom, "errorFrom");
23968
+ function openMachineChannel(context, options) {
23969
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
23970
+ const clearTimer = options.clearTimer ?? ((handle2) => clearTimeout(handle2));
23971
+ const inFlight = /* @__PURE__ */ new Map();
23972
+ const reply = /* @__PURE__ */ __name((message) => {
23973
+ if (!context.sendJson(message) && typeof message.requestId === "string") {
23974
+ context.sendJson({
23975
+ requestId: message.requestId,
23976
+ error: makeFleetLinkError("internal", "the reply could not be sent")
23977
+ });
23978
+ }
23979
+ }, "reply");
23980
+ const refuse = /* @__PURE__ */ __name((requestId, error) => {
23981
+ context.sendJson({ requestId, error });
23982
+ }, "refuse");
23983
+ const handle = /* @__PURE__ */ __name((raw) => {
23984
+ const request = asFleetLinkRequest(raw);
23985
+ if (!request) {
23986
+ if (isFleetLinkId(raw.requestId)) {
23987
+ refuse(
23988
+ raw.requestId,
23989
+ makeFleetLinkError("invalid", "a request needs a requestId, a type and object params")
23990
+ );
23991
+ }
23992
+ return;
23993
+ }
23994
+ const { requestId, type: name } = request;
23995
+ if (inFlight.has(requestId)) {
23996
+ refuse(requestId, makeFleetLinkError("invalid", "this requestId is already in flight"));
23997
+ return;
23998
+ }
23999
+ const verb = options.verb(name);
24000
+ if (!verb) {
24001
+ refuse(
24002
+ requestId,
24003
+ makeFleetLinkError(
24004
+ "unsupported_on_node",
24005
+ `${context.nodeName} (node ${context.nodeVersion}) cannot do this yet: ${name}`,
24006
+ { verb: name, nodeVersion: context.nodeVersion }
24007
+ )
24008
+ );
24009
+ return;
24010
+ }
24011
+ const consent = fleetLinkVerbConsent(name);
24012
+ if (!fleetConsentSatisfies(context.consents, consent)) {
24013
+ refuse(
24014
+ requestId,
24015
+ makeFleetLinkError("consent_missing", fleetConsentMissingHint(consent, context.nodeName), {
24016
+ verb: name,
24017
+ consent
24018
+ })
24019
+ );
24020
+ return;
24021
+ }
24022
+ if (verb.stateChanging && context.control.paused) {
24023
+ refuse(requestId, makeFleetLinkError("kill_switch", "clear it under Settings \u2192 Fleet"));
24024
+ return;
24025
+ }
24026
+ if (inFlight.size >= MACHINE_CHANNEL_MAX_IN_FLIGHT) {
24027
+ refuse(
24028
+ requestId,
24029
+ makeFleetLinkError(
24030
+ "busy",
24031
+ `${MACHINE_CHANNEL_MAX_IN_FLIGHT} requests are already running on this channel`
24032
+ )
24033
+ );
24034
+ return;
24035
+ }
24036
+ const abort = new AbortController();
24037
+ inFlight.set(requestId, abort);
24038
+ let operationId = null;
24039
+ let lastProgress = null;
24040
+ let settled = false;
24041
+ const becomeOperation = /* @__PURE__ */ __name(() => {
24042
+ if (settled || operationId !== null) return;
24043
+ inFlight.delete(requestId);
24044
+ operationId = context.operations.start(() => abort.abort());
24045
+ reply({ requestId, operationId });
24046
+ if (lastProgress) context.operations.progress(operationId, lastProgress);
24047
+ }, "becomeOperation");
24048
+ const inlineMs = Math.max(0, options.inlineReplyMs?.() ?? FLEET_LINK_INLINE_REPLY_MS);
24049
+ const timer = setTimer(becomeOperation, inlineMs);
24050
+ const finish = /* @__PURE__ */ __name((outcome) => {
24051
+ if (settled) return;
24052
+ settled = true;
24053
+ clearTimer(timer);
24054
+ if (operationId === null) {
24055
+ inFlight.delete(requestId);
24056
+ if (abort.signal.aborted) return;
24057
+ if (outcome.error) refuse(requestId, outcome.error);
24058
+ else reply({ requestId, result: outcome.result ?? null });
24059
+ return;
24060
+ }
24061
+ context.operations.done(operationId, outcome);
24062
+ }, "finish");
24063
+ let running;
24064
+ try {
24065
+ running = Promise.resolve(
24066
+ verb.run({
24067
+ verb: name,
24068
+ params: request.params ?? {},
24069
+ consents: context.consents,
24070
+ control: context.control,
24071
+ nodeVersion: context.nodeVersion,
24072
+ nodeName: context.nodeName,
24073
+ signal: abort.signal,
24074
+ logger: context.logger,
24075
+ progress: /* @__PURE__ */ __name((progress) => {
24076
+ lastProgress = progress;
24077
+ if (operationId !== null) context.operations.progress(operationId, progress);
24078
+ }, "progress")
24079
+ })
24080
+ );
24081
+ } catch (thrown) {
24082
+ running = Promise.reject(thrown);
24083
+ }
24084
+ running.then(
24085
+ (result) => finish({ result }),
24086
+ (thrown) => finish({ error: errorFrom(thrown, abort.signal) })
24087
+ );
24088
+ }, "handle");
24089
+ return {
24090
+ resumeKey: "machine",
24091
+ onJson: handle,
24092
+ onClose: /* @__PURE__ */ __name(() => {
24093
+ for (const abort of inFlight.values()) abort.abort();
24094
+ inFlight.clear();
24095
+ }, "onClose")
24096
+ };
24097
+ }
24098
+ __name(openMachineChannel, "openMachineChannel");
24099
+ function createMachineModule(options) {
24100
+ return {
24101
+ id: "machine",
24102
+ families: [
24103
+ {
24104
+ // Opening needs only the link (`fleetLinkChannelOpenConsent`, CONTRACTS
24105
+ // R-31); every request is checked against its verb's consent above.
24106
+ family: "machine",
24107
+ open: /* @__PURE__ */ __name((context) => ({ ok: true, handler: openMachineChannel(context, options) }), "open")
24108
+ }
24109
+ ]
24110
+ };
24111
+ }
24112
+ __name(createMachineModule, "createMachineModule");
24113
+
24114
+ // src/core/link/modules/index.ts
24115
+ var NODE_LINK_MODULES = [];
24116
+ function composeNodeLinkModules(options = {}) {
24117
+ const { extra = [], ...machineOptions } = options;
24118
+ let composed = null;
24119
+ const machine = createMachineModule({
24120
+ ...machineOptions,
24121
+ verb: /* @__PURE__ */ __name((name) => composed?.machineVerb(name), "verb")
24122
+ });
24123
+ composed = composeNodeModules([controlModule, machine, ...NODE_LINK_MODULES, ...extra]);
24124
+ return composed;
24125
+ }
24126
+ __name(composeNodeLinkModules, "composeNodeLinkModules");
24127
+
24128
+ // src/core/link/link-client.ts
24129
+ var MAX_PENDING_OPERATION_RESULTS = 64;
24130
+ var READY_TIMEOUT_MS = FLEET_LINK_AUTH_TIMEOUT_MS * 2;
24131
+ var MAX_HELLO_TAGS = 64;
24132
+ function createNodeLink(options) {
24133
+ return new NodeLinkClient(options);
24134
+ }
24135
+ __name(createNodeLink, "createNodeLink");
24136
+ var NodeLinkClient = class {
24137
+ constructor(options) {
24138
+ this.options = options;
24139
+ this.scheduler = options.scheduler ?? systemScheduler;
24140
+ this.now = options.now ?? (() => Date.now());
24141
+ this.backoff = new LinkBackoff({ random: options.random });
24142
+ this.modules = options.modules ?? composeNodeLinkModules({
24143
+ inlineReplyMs: /* @__PURE__ */ __name(() => this.limits.inlineReplyMs, "inlineReplyMs"),
24144
+ setTimer: /* @__PURE__ */ __name((callback, ms) => this.scheduler.setTimeout(callback, ms), "setTimer"),
24145
+ clearTimer: /* @__PURE__ */ __name((handle) => this.scheduler.clearTimeout(handle), "clearTimer")
24146
+ });
24147
+ const operations = {
24148
+ start: /* @__PURE__ */ __name((cancel) => {
24149
+ const id = `op-${(0, import_node_crypto3.randomUUID)()}`;
24150
+ this.operations.set(id, { cancel });
24151
+ return id;
24152
+ }, "start"),
24153
+ progress: /* @__PURE__ */ __name((operationId, progress) => {
24154
+ if (!this.operations.has(operationId)) return;
24155
+ if (this.state.phase === "connected") {
24156
+ this.mux.sendControl({ type: "operation.progress", operationId, progress });
24157
+ }
24158
+ }, "progress"),
24159
+ done: /* @__PURE__ */ __name((operationId, outcome) => {
24160
+ if (!this.operations.delete(operationId)) return;
24161
+ let done = { type: "operation.done", operationId };
24162
+ if (outcome.error) done.error = outcome.error;
24163
+ else done.result = outcome.result ?? null;
24164
+ if (encodeFleetLinkControl(done) === null) {
24165
+ done = {
24166
+ type: "operation.done",
24167
+ operationId,
24168
+ error: makeFleetLinkError(
24169
+ "operation_failed",
24170
+ `the result was too large to send back (over ${Math.round(FLEET_LINK_MAX_JSON_BYTES / (1024 * 1024))} MiB)`
24171
+ )
24172
+ };
24173
+ }
24174
+ if (this.state.phase !== "connected" || !this.mux.sendControl(done)) this.keepResult(done);
24175
+ }, "done")
24176
+ };
24177
+ this.mux = new ChannelMux({
24178
+ modules: this.modules,
24179
+ consents: options.consents,
24180
+ control: this.control,
24181
+ operations,
24182
+ nodeVersion: options.nodeVersion,
24183
+ nodeName: options.nodeName?.trim() || "this computer",
24184
+ logger: options.logger
24185
+ });
24186
+ this.control.onChange(({ paused }) => this.patch({ paused }));
24187
+ }
24188
+ options;
24189
+ static {
24190
+ __name(this, "NodeLinkClient");
24191
+ }
24192
+ control = createLinkControlState();
24193
+ modules;
24194
+ scheduler;
24195
+ now;
24196
+ backoff;
24197
+ mux;
24198
+ listeners = /* @__PURE__ */ new Set();
24199
+ operations = /* @__PURE__ */ new Map();
24200
+ pendingResults = [];
24201
+ limits = FLEET_LINK_DEFAULT_LIMITS;
24202
+ started = false;
24203
+ closed = false;
24204
+ socket = null;
24205
+ reconnectTimer = null;
24206
+ tickTimer = null;
24207
+ readyTimer = null;
24208
+ resumeExpiry = null;
24209
+ lastFrameAt = 0;
24210
+ /** The generation to present on the next hello (the last one this node held). */
24211
+ previousGeneration = null;
24212
+ resumeChannels = [];
24213
+ refusalLogged = false;
24214
+ /** The single-use token of the attempt in flight, protected in the logger until the attempt ends. */
24215
+ attemptToken = null;
24216
+ dropLogged = false;
24217
+ state = {
24218
+ phase: "idle",
24219
+ detail: null,
24220
+ linkGeneration: null,
24221
+ connectedSince: null,
24222
+ lastDisconnectAt: null,
24223
+ bytesIn: 0,
24224
+ bytesOut: 0,
24225
+ openChannels: 0,
24226
+ paused: false,
24227
+ nextAttemptInMs: null
24228
+ };
24229
+ getState() {
24230
+ return { ...this.state, openChannels: this.mux.size };
24231
+ }
24232
+ onChange(listener) {
24233
+ this.listeners.add(listener);
24234
+ return () => {
24235
+ this.listeners.delete(listener);
24236
+ };
24237
+ }
24238
+ start() {
24239
+ if (this.started || this.closed) return;
24240
+ this.started = true;
24241
+ if (!isFleetLinkEligible(this.options.consents)) {
24242
+ this.patch({
24243
+ phase: "not-open",
24244
+ detail: "not open: this computer only reports health (start it with --work, --attend or --manage)"
24245
+ });
24246
+ this.options.logger?.info(
24247
+ "Node link not opened: no --work, --attend or --manage consent \u2014 this computer only reports health."
24248
+ );
24249
+ return;
24250
+ }
24251
+ if (!isSecureLinkOrigin(this.options.apiUrl)) {
24252
+ this.patch({ phase: "insecure", detail: "not open: the platform is reached over plain http" });
24253
+ this.options.logger?.warn(
24254
+ "Node link not opened: the platform is reached over plain http, and a link token is only sent over https (or to localhost)."
24255
+ );
24256
+ return;
24257
+ }
24258
+ if (!this.options.factory) {
24259
+ this.patch({ phase: "unavailable", detail: "not open: this runtime has no WebSocket client" });
24260
+ this.options.logger?.warn("Node link not opened: this runtime has no WebSocket client.");
24261
+ return;
24262
+ }
24263
+ void this.connect();
24264
+ }
24265
+ close() {
24266
+ if (this.closed) return;
24267
+ this.closed = true;
24268
+ this.clearTimer("reconnectTimer");
24269
+ this.clearTimer("tickTimer");
24270
+ this.clearTimer("readyTimer");
24271
+ this.clearTimer("resumeExpiry");
24272
+ this.mux.closeAll("normal");
24273
+ const socket = this.socket;
24274
+ this.socket = null;
24275
+ this.mux.bind(null);
24276
+ if (socket) {
24277
+ this.detach(socket);
24278
+ try {
24279
+ socket.close(FLEET_LINK_CLOSE_CODES.normal, "node shutting down");
24280
+ } catch {
24281
+ }
24282
+ }
24283
+ this.forgetToken();
24284
+ for (const operation of this.operations.values()) operation.cancel();
24285
+ this.operations.clear();
24286
+ this.patch({ phase: "closed", detail: null, nextAttemptInMs: null, linkGeneration: null });
24287
+ }
24288
+ // ─── Connecting ────────────────────────────────────────────────────────
24289
+ async connect() {
24290
+ if (this.closed) return;
24291
+ this.patch({
24292
+ phase: this.previousGeneration === null ? "connecting" : "reconnecting",
24293
+ nextAttemptInMs: null
24294
+ });
24295
+ let minted;
24296
+ try {
24297
+ minted = await this.options.mintToken();
24298
+ } catch (error) {
24299
+ if (this.closed) return;
24300
+ if (error instanceof FleetClientError && error.status === 404) {
24301
+ this.refused();
24302
+ return;
24303
+ }
24304
+ this.options.logger?.warn(`Node link: could not get a link token: ${describe4(error)}`);
24305
+ this.scheduleReconnect(this.backoff.next());
24306
+ return;
24307
+ }
24308
+ if (this.closed) return;
24309
+ this.forgetToken();
24310
+ this.attemptToken = minted.token;
24311
+ this.options.logger?.protect(minted.token);
24312
+ let capabilities;
24313
+ try {
24314
+ capabilities = [...await this.options.capabilities()].slice(0, MAX_HELLO_TAGS);
24315
+ } catch {
24316
+ capabilities = [];
24317
+ }
24318
+ if (this.closed) return;
24319
+ let socket;
24320
+ try {
24321
+ socket = this.options.factory(toLinkWsUrl(this.options.apiUrl, minted.wsPath || FLEET_LINK_WS_PATH));
24322
+ } catch (error) {
24323
+ this.forgetToken();
24324
+ this.options.logger?.warn(`Node link: could not open a socket: ${describe4(error)}`);
24325
+ this.scheduleReconnect(this.backoff.next());
24326
+ return;
24327
+ }
24328
+ this.socket = socket;
24329
+ this.patch({ bytesIn: 0, bytesOut: 0 });
24330
+ socket.onopen = () => {
24331
+ if (this.socket !== socket) return;
24332
+ const hello = {
24333
+ type: "link.hello",
24334
+ nodeId: this.options.nodeId,
24335
+ linkVersion: FLEET_LINK_VERSION,
24336
+ capabilities,
24337
+ consents: { ...this.options.consents },
24338
+ token: minted.token,
24339
+ nodeVersion: this.options.nodeVersion
24340
+ };
24341
+ if (this.previousGeneration !== null) {
24342
+ hello.resume = { linkGeneration: this.previousGeneration, channels: this.resumeChannels };
24343
+ }
24344
+ this.mux.bind((frame) => this.sendOn(socket, frame));
24345
+ this.mux.sendControl(hello);
24346
+ this.lastFrameAt = this.now();
24347
+ this.clearTimer("readyTimer");
24348
+ this.readyTimer = this.scheduler.setTimeout(() => {
24349
+ this.readyTimer = null;
24350
+ if (this.socket === socket && this.state.phase !== "connected") {
24351
+ this.dropSocket(socket, FLEET_LINK_CLOSE_CODES.protocol, "no link.ready");
24352
+ }
24353
+ }, READY_TIMEOUT_MS);
24354
+ };
24355
+ socket.onmessage = (event) => {
24356
+ if (this.socket === socket) this.onBytes(event.data);
24357
+ };
24358
+ socket.onerror = () => void 0;
24359
+ socket.onclose = (event) => {
24360
+ if (this.socket === socket) this.dropSocket(socket, linkCloseCode(event), null);
24361
+ };
24362
+ }
24363
+ refused() {
24364
+ this.patch({
24365
+ phase: "refused",
24366
+ detail: "refused by the platform: this deployment does not accept node links yet"
24367
+ });
24368
+ if (!this.refusalLogged) {
24369
+ this.refusalLogged = true;
24370
+ this.options.logger?.info("The platform does not accept node links yet \u2014 trying again in 5 minutes.");
24371
+ }
24372
+ this.scheduleReconnect(FLEET_LINK_REFUSED_RETRY_MS);
24373
+ }
24374
+ scheduleReconnect(delayMs) {
24375
+ if (this.closed) return;
24376
+ this.clearTimer("reconnectTimer");
24377
+ this.patch({ nextAttemptInMs: delayMs });
24378
+ this.reconnectTimer = this.scheduler.setTimeout(() => {
24379
+ this.reconnectTimer = null;
24380
+ void this.connect();
24381
+ }, delayMs);
24382
+ }
24383
+ // ─── Receiving ─────────────────────────────────────────────────────────
24384
+ onBytes(data) {
24385
+ const bytes = linkMessageBytes(data);
24386
+ if (!bytes) return;
24387
+ this.lastFrameAt = this.now();
24388
+ this.state.bytesIn += bytes.byteLength;
24389
+ if (bytes.byteLength > FLEET_LINK_MAX_FRAME_BYTES) {
24390
+ const channel = peekFleetLinkFrameChannel(bytes);
24391
+ if (channel !== null && channel > 0) this.mux.closeLocal(channel, "invalid", "frame over 16 MiB");
24392
+ return;
24393
+ }
24394
+ const frame = decodeFleetLinkFrame(bytes);
24395
+ if (!frame) return;
24396
+ if (frame.channel !== 0) {
24397
+ if (this.state.phase === "connected") this.mux.deliver(frame);
24398
+ return;
24399
+ }
24400
+ const message = decodeFleetLinkControl(frame.payload);
24401
+ if (!message) return;
24402
+ if (isFleetLinkControlMessage(message)) this.onControl(message);
24403
+ else if (this.state.phase === "connected") this.onExtension(message);
24404
+ }
24405
+ onControl(message) {
24406
+ if (this.state.phase !== "connected") {
24407
+ if (message.type === "link.ready") this.onReady(message);
24408
+ return;
24409
+ }
24410
+ switch (message.type) {
24411
+ case "link.ping":
24412
+ this.mux.sendControl({ type: "link.pong", t: message.t });
24413
+ return;
24414
+ case "link.pause":
24415
+ if (!this.control.paused) {
24416
+ this.options.logger?.warn("The fleet kill switch is on: live actions on this computer are paused.");
24417
+ }
24418
+ this.control.pause();
24419
+ return;
24420
+ case "link.resume":
24421
+ if (this.control.paused)
24422
+ this.options.logger?.info("The fleet kill switch was cleared: live actions resume.");
24423
+ this.control.resume();
24424
+ return;
24425
+ case "link.drain":
24426
+ this.options.logger?.warn(`Drain all: owner sessions on this computer end in ${message.graceSec} s.`);
24427
+ this.control.startDrain(message, this.now());
24428
+ return;
24429
+ case "channel.open":
24430
+ this.mux.open(message).catch((error) => {
24431
+ this.options.logger?.warn(
24432
+ `Node link: opening channel ${message.channel} failed: ${describe4(error)}`
24433
+ );
24434
+ });
24435
+ return;
24436
+ case "channel.close":
24437
+ this.mux.closeRemote(message.channel, message.reason);
24438
+ return;
24439
+ case "operation.cancel":
24440
+ this.operations.get(message.operationId)?.cancel();
24441
+ return;
24442
+ default:
24443
+ return;
24444
+ }
24445
+ }
24446
+ onReady(message) {
24447
+ const socket = this.socket;
24448
+ if (!socket) return;
24449
+ this.clearTimer("readyTimer");
24450
+ this.clearTimer("resumeExpiry");
24451
+ this.forgetToken();
24452
+ this.limits = message.limits;
24453
+ this.previousGeneration = message.linkGeneration;
24454
+ if (this.control.paused) {
24455
+ this.control.resume();
24456
+ this.options.logger?.info(
24457
+ "Node link reconnected: the earlier pause is lifted unless the platform repeats it."
24458
+ );
24459
+ }
24460
+ this.resumeChannels = [];
24461
+ this.backoff.reset();
24462
+ this.refusalLogged = false;
24463
+ this.dropLogged = false;
24464
+ this.patch({
24465
+ phase: "connected",
24466
+ detail: null,
24467
+ linkGeneration: message.linkGeneration,
24468
+ connectedSince: this.now(),
24469
+ nextAttemptInMs: null
24470
+ });
24471
+ this.mux.resumeGeneration(message.resumed, message.closed);
24472
+ this.options.logger?.info(
24473
+ `Node link connected (generation ${message.linkGeneration}${message.resumed.length > 0 ? `, ${message.resumed.length} channel(s) resumed` : ""}).`
24474
+ );
24475
+ this.flushResults();
24476
+ this.scheduleTick();
24477
+ }
24478
+ onExtension(message) {
24479
+ const handler = this.modules.controlHandler(message.type);
24480
+ if (!handler) return;
24481
+ try {
24482
+ handler(message, { consents: this.options.consents, control: this.control, logger: this.options.logger });
24483
+ } catch (error) {
24484
+ this.options.logger?.warn(`Node link: ${message.type} handler failed: ${describe4(error)}`);
24485
+ }
24486
+ }
24487
+ // ─── Keeping it alive ──────────────────────────────────────────────────
24488
+ scheduleTick() {
24489
+ this.clearTimer("tickTimer");
24490
+ this.tickTimer = this.scheduler.setTimeout(() => {
24491
+ this.tickTimer = null;
24492
+ this.tick();
24493
+ }, this.limits.pingIntervalMs);
24494
+ }
24495
+ tick() {
24496
+ const socket = this.socket;
24497
+ if (!socket || this.state.phase !== "connected") return;
24498
+ if (this.now() - this.lastFrameAt >= this.limits.silenceMs) {
24499
+ this.options.logger?.warn(
24500
+ `Node link: nothing heard for ${Math.round(this.limits.silenceMs / 1e3)} s \u2014 reconnecting.`
24501
+ );
24502
+ this.dropSocket(socket, FLEET_LINK_CLOSE_CODES.silence, "silence");
24503
+ return;
24504
+ }
24505
+ this.mux.sendControl({ type: "link.ping", t: this.now() });
24506
+ this.scheduleTick();
24507
+ }
24508
+ /**
24509
+ * The socket is gone (closed by the platform, by silence, or by us). Run
24510
+ * the drop once: suspend resumable channels, then decide whether and when
24511
+ * to come back.
24512
+ */
24513
+ dropSocket(socket, code, reason) {
24514
+ if (this.socket !== socket) return;
24515
+ this.socket = null;
24516
+ this.detach(socket);
24517
+ this.forgetToken();
24518
+ if (reason !== null) {
24519
+ try {
24520
+ socket.close(code ?? FLEET_LINK_CLOSE_CODES.normal, reason);
24521
+ } catch {
24522
+ }
24523
+ }
24524
+ this.clearTimer("tickTimer");
24525
+ this.clearTimer("readyTimer");
24526
+ const wasConnected = this.state.phase === "connected";
24527
+ this.resumeChannels = this.mux.suspend();
24528
+ this.patch({ linkGeneration: null, connectedSince: null, lastDisconnectAt: this.now() });
24529
+ if (this.closed) return;
24530
+ if (this.resumeChannels.length > 0) this.armResumeExpiry();
24531
+ if (code === FLEET_LINK_CLOSE_CODES.disabled) {
24532
+ this.refused();
24533
+ return;
24534
+ }
24535
+ if (code === FLEET_LINK_CLOSE_CODES.noConsent) {
24536
+ this.mux.closeAll("consent_missing");
24537
+ this.patch({ phase: "not-open", detail: "refused by the platform: no consent that opens a link" });
24538
+ this.options.logger?.warn("The platform refused the node link: no --work, --attend or --manage consent.");
24539
+ return;
24540
+ }
24541
+ if (code === FLEET_LINK_CLOSE_CODES.replaced) {
24542
+ this.options.logger?.warn("Node link replaced by a newer link for this node \u2014 is it running twice?");
24543
+ }
24544
+ const wait = code === FLEET_LINK_CLOSE_CODES.rateLimited ? Math.max(this.backoff.next(), FLEET_LINK_HELLO_BLOCK_MS) : this.backoff.next();
24545
+ if (wasConnected && !this.dropLogged) {
24546
+ this.dropLogged = true;
24547
+ this.options.logger?.warn(
24548
+ `Node link lost${code !== null ? ` (code ${code})` : ""} \u2014 reconnecting in ${Math.round(wait / 1e3)} s.`
24549
+ );
24550
+ }
24551
+ this.patch({ phase: "reconnecting", detail: "reconnecting" });
24552
+ this.scheduleReconnect(wait);
24553
+ }
24554
+ /** Channels held for a reconnect close `resume_failed` if none comes within the window. */
24555
+ armResumeExpiry() {
24556
+ this.clearTimer("resumeExpiry");
24557
+ this.resumeExpiry = this.scheduler.setTimeout(() => {
24558
+ this.resumeExpiry = null;
24559
+ if (this.state.phase === "connected") return;
24560
+ this.mux.closeAll("resume_failed");
24561
+ this.resumeChannels = [];
24562
+ }, FLEET_LINK_RESUME_WINDOW_MS);
24563
+ }
24564
+ // ─── Sending ───────────────────────────────────────────────────────────
24565
+ sendOn(socket, frame) {
24566
+ if (this.socket !== socket || socket.readyState !== LINK_SOCKET_OPEN) return false;
24567
+ try {
24568
+ socket.send(frame);
24569
+ } catch {
24570
+ return false;
24571
+ }
24572
+ this.state.bytesOut += frame.byteLength;
24573
+ return true;
24574
+ }
24575
+ keepResult(done) {
24576
+ this.pendingResults.push(done);
24577
+ if (this.pendingResults.length > MAX_PENDING_OPERATION_RESULTS) this.pendingResults.shift();
24578
+ }
24579
+ flushResults() {
24580
+ const pending = this.pendingResults;
24581
+ this.pendingResults = [];
24582
+ for (const done of pending) {
24583
+ if (!this.mux.sendControl(done)) this.keepResult(done);
24584
+ }
24585
+ }
24586
+ // ─── Plumbing ──────────────────────────────────────────────────────────
24587
+ /** A link token is single-use: once its attempt ended it is dead, and need not stay in the redactor. */
24588
+ forgetToken() {
24589
+ if (this.attemptToken === null) return;
24590
+ this.options.logger?.unprotect(this.attemptToken);
24591
+ this.attemptToken = null;
24592
+ }
24593
+ detach(socket) {
24594
+ socket.onopen = null;
24595
+ socket.onmessage = null;
24596
+ socket.onclose = null;
24597
+ socket.onerror = null;
24598
+ this.mux.bind(null);
24599
+ }
24600
+ clearTimer(name) {
24601
+ const handle = this[name];
24602
+ if (handle !== null) {
24603
+ this.scheduler.clearTimeout(handle);
24604
+ this[name] = null;
24605
+ }
24606
+ }
24607
+ patch(next) {
24608
+ this.state = { ...this.state, ...next };
24609
+ const snapshot = this.getState();
24610
+ for (const listener of [...this.listeners]) {
24611
+ try {
24612
+ listener(snapshot);
24613
+ } catch {
24614
+ }
24615
+ }
24616
+ }
24617
+ };
24618
+ function describe4(error) {
24619
+ return error instanceof Error ? error.message : String(error);
24620
+ }
24621
+ __name(describe4, "describe");
24622
+
24623
+ // src/core/link/link-record.ts
24624
+ var PHASES = /* @__PURE__ */ new Set([
24625
+ "idle",
24626
+ "not-open",
24627
+ "insecure",
24628
+ "unavailable",
24629
+ "connecting",
24630
+ "connected",
24631
+ "reconnecting",
24632
+ "refused",
24633
+ "closed"
24634
+ ]);
24635
+ function nodeLinkRecordPath(configPath) {
24636
+ return `${configPath}.link.json`;
24637
+ }
24638
+ __name(nodeLinkRecordPath, "nodeLinkRecordPath");
24639
+ function toNodeLinkRecord(consents, state, now) {
24640
+ return {
24641
+ version: 1,
24642
+ recordedAt: new Date(now).toISOString(),
24643
+ consents: { ...consents },
24644
+ phase: state.phase,
24645
+ detail: state.detail,
24646
+ linkGeneration: state.linkGeneration,
24647
+ connectedSince: state.connectedSince === null ? null : new Date(state.connectedSince).toISOString()
24648
+ };
24649
+ }
24650
+ __name(toNodeLinkRecord, "toNodeLinkRecord");
24651
+ function parseNodeLinkRecord(raw) {
24652
+ if (!raw) return null;
24653
+ let parsed;
24654
+ try {
24655
+ parsed = JSON.parse(raw);
24656
+ } catch {
24657
+ return null;
24658
+ }
24659
+ if (!parsed || typeof parsed !== "object") return null;
24660
+ const record = parsed;
24661
+ const consents = normalizeFleetConsents(record.consents);
24662
+ if (record.version !== 1 || typeof record.recordedAt !== "string" || !Number.isFinite(Date.parse(record.recordedAt)) || consents === null || typeof record.phase !== "string" || !PHASES.has(record.phase)) {
24663
+ return null;
24664
+ }
24665
+ return {
24666
+ version: 1,
24667
+ recordedAt: record.recordedAt,
24668
+ consents,
24669
+ phase: record.phase,
24670
+ detail: typeof record.detail === "string" ? record.detail.slice(0, 300) : null,
24671
+ linkGeneration: typeof record.linkGeneration === "number" && Number.isSafeInteger(record.linkGeneration) ? record.linkGeneration : null,
24672
+ connectedSince: typeof record.connectedSince === "string" && Number.isFinite(Date.parse(record.connectedSince)) ? record.connectedSince : null
24673
+ };
24674
+ }
24675
+ __name(parseNodeLinkRecord, "parseNodeLinkRecord");
24676
+ async function readNodeLinkRecord(fs10, configPath) {
24677
+ try {
24678
+ return parseNodeLinkRecord(await fs10.readFile(nodeLinkRecordPath(configPath)));
24679
+ } catch {
24680
+ return null;
24681
+ }
24682
+ }
24683
+ __name(readNodeLinkRecord, "readNodeLinkRecord");
24684
+ async function writeNodeLinkRecord(fs10, configPath, record) {
24685
+ const path2 = nodeLinkRecordPath(configPath);
24686
+ await fs10.mkdir(fs10.dirname(path2));
24687
+ await fs10.writeFile(path2, `${JSON.stringify(record, null, 2)}
24688
+ `);
24689
+ }
24690
+ __name(writeNodeLinkRecord, "writeNodeLinkRecord");
24691
+ function describeConsents(consents) {
24692
+ const on = ["work", "attend", "manage", "usageScan"].filter((key) => consents[key]).map((key) => key === "usageScan" ? "usage-scan" : key);
24693
+ return on.length > 0 ? on.join(" \xB7 ") : "none (enrolled only \u2014 reports health)";
24694
+ }
24695
+ __name(describeConsents, "describeConsents");
24696
+ function describeLinkRecord(record, now) {
24697
+ if (!record) return "unknown (the service has not reported its link yet)";
24698
+ const age = Math.max(0, Math.round((now - Date.parse(record.recordedAt)) / 1e3));
24699
+ const heard = `as of ${record.recordedAt}${age > 120 ? ` (${age}s ago)` : ""}`;
24700
+ switch (record.phase) {
24701
+ case "connected":
24702
+ return `connected since ${record.connectedSince ?? record.recordedAt} (generation ${record.linkGeneration ?? "?"}) \u2014 ${heard}`;
24703
+ case "reconnecting":
24704
+ case "connecting":
24705
+ return `${record.phase}\u2026 \u2014 ${heard}`;
24706
+ default:
24707
+ return `${record.detail ?? record.phase} \u2014 ${heard}`;
24708
+ }
24709
+ }
24710
+ __name(describeLinkRecord, "describeLinkRecord");
24711
+
24712
+ // src/core/runtime.ts
24713
+ function buildSelfDescriptionTelemetry(io, cliCompat) {
24714
+ const pinned = /* @__PURE__ */ __name(() => io.environment.modelCli ?? {}, "pinned");
24715
+ const telemetry = {
24716
+ cliVersion: cliCompat ? async () => primaryPinnedCliVersion(await cliCompat.probeAll(pinned())) ?? detectAgentCliVersion(io.runner) : () => detectAgentCliVersion(io.runner),
24717
+ modelIdentity: cacheProbe(() => detectModelIdentity(io.runner, io.environment.modelCli ?? {}))
24718
+ };
24719
+ if (cliCompat) {
24720
+ telemetry.cliVersions = async () => describeCliVersions(await cliCompat.probeAll(pinned()));
24721
+ }
24722
+ if (io.diskProbe) {
24723
+ const probe = io.diskProbe;
24724
+ const path2 = io.workspacePath ?? process.cwd();
24725
+ telemetry.diskFreeBytes = () => measureWorkspaceFreeBytes(probe, path2);
24726
+ }
24727
+ return telemetry;
24728
+ }
24729
+ __name(buildSelfDescriptionTelemetry, "buildSelfDescriptionTelemetry");
24730
+ function clampHeartbeatInterval(intervalMs) {
24731
+ if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs)) {
24732
+ return DEFAULT_HEARTBEAT_INTERVAL_MS;
24733
+ }
24734
+ return Math.min(Math.max(Math.round(intervalMs), MIN_HEARTBEAT_INTERVAL_MS), MAX_HEARTBEAT_INTERVAL_MS);
24735
+ }
24736
+ __name(clampHeartbeatInterval, "clampHeartbeatInterval");
24737
+ async function enrollNode(options) {
24738
+ const { logger } = options;
24739
+ logger.protect(options.token);
24740
+ const client = new FleetClient({
24741
+ apiUrl: options.apiUrl,
24742
+ fetchFn: options.fetchFn,
24743
+ logger,
24744
+ userAgent: options.userAgent ?? `ever-works-node/${options.version}`
24745
+ });
24746
+ const description = await describeSelf(
24747
+ options.runner,
24748
+ options.environment,
24749
+ options.version,
24750
+ options.capabilitySelection ?? null,
24751
+ // Deliberately WITHOUT the pinned-CLI compat probe (review, slice AR):
24752
+ // that probe adds `cliVersions`, an optional field an older platform's
24753
+ // `forbidNonWhitelisted` pipe answers with a 400 — and enrollment, unlike
24754
+ // the heartbeat, has no strip-and-retry fallback, so a new node could
24755
+ // never enroll against it. The first heartbeat reports the pinned
24756
+ // versions, behind the heartbeat's own fallback.
24757
+ buildSelfDescriptionTelemetry(options)
24758
+ );
24759
+ logger.info(`Enrolling with ${client.baseUrl} as ${description.platform} [${description.capabilities.join(", ")}]`);
24760
+ const result = await client.enroll({ token: options.token, ...description });
24761
+ logger.protect(result.secret);
24762
+ const limits = clampResourceLimits(options.limits);
24763
+ const config = {
24764
+ apiUrl: client.baseUrl,
24765
+ nodeId: result.nodeId,
24766
+ secret: result.secret,
24767
+ kind: options.kind,
24768
+ capabilities: description.capabilities,
24769
+ limits,
24770
+ heartbeatIntervalMs: clampHeartbeatInterval(options.heartbeatIntervalMs),
24771
+ enrolledAt: new Date(options.now ? options.now() : Date.now()).toISOString()
24772
+ };
24773
+ if (options.capabilitySelection) {
24774
+ config.capabilitySelection = [...options.capabilitySelection];
24775
+ }
24776
+ const label = options.name?.trim() || result.node.name;
24777
+ if (label) {
24778
+ config.name = label;
24779
+ }
24780
+ logger.info(
24781
+ `Enrolled as node ${result.nodeId} ("${config.name ?? "unnamed"}") \u2014 max ${limits.maxConcurrentJobs} concurrent job(s)`
24782
+ );
24783
+ return config;
24784
+ }
24785
+ __name(enrollNode, "enrollNode");
24786
+ function createNodeRuntime(config, io, options = {}) {
24787
+ io.logger.protect(config.secret);
24788
+ const userAgent = io.userAgent ?? `ever-works-node/${io.version}`;
24789
+ const apiBase = resolveApiBase(config, options.env ?? process.env);
24790
+ io.logger.info(`Control plane: ${describeApiBase(apiBase)}`);
24791
+ if (apiBase.mismatch) {
24792
+ io.logger.warn(
24793
+ `The pinned control plane (${apiBase.url}) is not the origin this node enrolled against (${apiBase.configuredUrl}). Every call will be refused with 401 until the pin is corrected or the node is re-enrolled.`
24794
+ );
24795
+ }
24796
+ const client = new FleetClient({
24797
+ apiUrl: apiBase.url,
24798
+ fetchFn: io.fetchFn,
24799
+ logger: io.logger,
24800
+ userAgent
24801
+ });
24802
+ const selection = config.capabilitySelection ?? null;
24803
+ const workspaceRoot = options.agentTaskWorkspaceRoot ?? defaultFleetTaskWorkspaceRoot();
24804
+ const executionPolicy = resolveNodeExecutionPolicy(config.executionPolicy);
24805
+ const cliCompat = new ModelCliCompatibilityProbe({
24806
+ runner: io.runner,
24807
+ platform: io.environment.platform,
24808
+ ...io.statFile ? { statFile: io.statFile } : {},
24809
+ ...io.now ? { now: io.now } : {}
24810
+ });
24811
+ const pinnedModelCli = options.modelCli ?? io.environment.modelCli ?? {};
24812
+ const telemetry = buildSelfDescriptionTelemetry(
24813
+ {
24814
+ ...io,
24815
+ workspacePath: io.workspacePath ?? workspaceRoot,
22157
24816
  environment: { ...io.environment, modelCli: pinnedModelCli }
22158
24817
  },
22159
24818
  cliCompat
22160
24819
  );
24820
+ let loopRef = null;
24821
+ if (io.health) {
24822
+ const probeHealth = createHealthProbe(io.health, {
24823
+ workspacePath: io.workspacePath ?? workspaceRoot,
24824
+ ...io.now ? { now: io.now } : {}
24825
+ });
24826
+ telemetry.health = () => loopRef && !loopRef.acceptsField("health") ? null : probeHealth();
24827
+ }
22161
24828
  const attendedCapture = options.attendEnabled ? selectAttendedCapture(io.environment, options) : null;
22162
- const environment = attendedCapture ? {
24829
+ const consents = {
24830
+ work: options.workerEnabled === true,
24831
+ attend: attendedCapture !== null,
24832
+ manage: options.manageEnabled === true,
24833
+ usageScan: false
24834
+ };
24835
+ const linkModules = composeNodeLinkModules({
24836
+ ...options.linkModules ? { extra: options.linkModules } : {},
24837
+ ...io.scheduler ? {
24838
+ setTimer: /* @__PURE__ */ __name((callback, ms) => io.scheduler.setTimeout(callback, ms), "setTimer"),
24839
+ clearTimer: /* @__PURE__ */ __name((handle) => io.scheduler.clearTimeout(handle), "clearTimer")
24840
+ } : {}
24841
+ });
24842
+ const linkModuleTags = linkModules.tags({ consents, platform: io.environment.platform, nodeVersion: io.version });
24843
+ const environment = {
22163
24844
  ...io.environment,
22164
- attended: true,
22165
- captureBackends: attendedCapture.captureBackend ? [attendedCapture.captureBackend] : []
22166
- } : io.environment;
24845
+ ...attendedCapture ? {
24846
+ attended: true,
24847
+ captureBackends: attendedCapture.captureBackend ? [attendedCapture.captureBackend] : []
24848
+ } : {},
24849
+ ...consents.manage ? { managed: true } : {},
24850
+ ...linkModuleTags.length > 0 ? { linkModuleTags } : {}
24851
+ };
24852
+ telemetry.consents = () => consents;
24853
+ let lastDescribedCapabilities = [];
24854
+ let startLink = null;
22167
24855
  let attendedWake = null;
22168
24856
  const holdableLanes = [];
22169
24857
  let limitTarget = null;
@@ -22183,7 +24871,11 @@ function createNodeRuntime(config, io, options = {}) {
22183
24871
  // Re-probed on EVERY beat, like the capability tags: installing an
22184
24872
  // agent CLI or filling a disk is exactly the kind of change an
22185
24873
  // operator needs to see without restarting the node.
22186
- describe: /* @__PURE__ */ __name(() => describeSelf(io.runner, environment, io.version, selection, telemetry), "describe"),
24874
+ describe: /* @__PURE__ */ __name(async () => {
24875
+ const description = await describeSelf(io.runner, environment, io.version, selection, telemetry);
24876
+ lastDescribedCapabilities = description.capabilities;
24877
+ return description;
24878
+ }, "describe"),
22187
24879
  intervalMs: clampHeartbeatInterval(config.heartbeatIntervalMs),
22188
24880
  logger: io.logger,
22189
24881
  ...io.scheduler ? { scheduler: io.scheduler } : {},
@@ -22192,9 +24884,12 @@ function createNodeRuntime(config, io, options = {}) {
22192
24884
  attendedWake?.(response.pendingComputerSessions);
22193
24885
  lifecycle.applyHeartbeat(response);
22194
24886
  throttleTarget?.(response.node?.modelThrottles);
24887
+ startLink?.();
22195
24888
  }, "onAccepted")
22196
24889
  };
22197
- const runtime = { client, loop: new HeartbeatLoop(loopOptions), lifecycle, cliCompat };
24890
+ const loop = new HeartbeatLoop(loopOptions);
24891
+ loopRef = loop;
24892
+ const runtime = { client, loop, lifecycle, cliCompat, consents };
22198
24893
  let sharedJobClient = null;
22199
24894
  const jobClientFor = /* @__PURE__ */ __name(() => sharedJobClient ??= new FleetJobClient({
22200
24895
  apiUrl: apiBase.url,
@@ -22204,6 +24899,46 @@ function createNodeRuntime(config, io, options = {}) {
22204
24899
  logger: io.logger,
22205
24900
  userAgent
22206
24901
  }), "jobClientFor");
24902
+ if (options.nodeLink) {
24903
+ const link = createNodeLink({
24904
+ apiUrl: apiBase.url,
24905
+ nodeId: config.nodeId,
24906
+ ...config.name ? { nodeName: config.name } : {},
24907
+ nodeVersion: io.version,
24908
+ consents,
24909
+ capabilities: /* @__PURE__ */ __name(() => lastDescribedCapabilities, "capabilities"),
24910
+ mintToken: /* @__PURE__ */ __name(() => jobClientFor().mintLinkToken(), "mintToken"),
24911
+ factory: options.linkSocketFactory === void 0 ? defaultLinkSocketFactory() : options.linkSocketFactory,
24912
+ modules: linkModules,
24913
+ logger: io.logger,
24914
+ ...io.scheduler ? { scheduler: io.scheduler } : {},
24915
+ ...io.now ? { now: io.now } : {}
24916
+ });
24917
+ runtime.link = link;
24918
+ let predatesLogged = false;
24919
+ startLink = /* @__PURE__ */ __name(() => {
24920
+ if (runtime.loop.acceptsField("consents")) {
24921
+ link.start();
24922
+ } else if (!predatesLogged) {
24923
+ predatesLogged = true;
24924
+ io.logger.warn("Node link not opened: the platform predates consents in the heartbeat.");
24925
+ }
24926
+ }, "startLink");
24927
+ if (options.persistLinkState) {
24928
+ const persist = options.persistLinkState;
24929
+ let lastKey = "";
24930
+ link.onChange((state) => {
24931
+ const key = `${state.phase}|${state.linkGeneration ?? ""}|${state.detail ?? ""}`;
24932
+ if (key === lastKey) return;
24933
+ lastKey = key;
24934
+ void Promise.resolve().then(() => persist(toNodeLinkRecord(consents, state, io.now ? io.now() : Date.now()))).catch(
24935
+ (error) => io.logger.warn(
24936
+ `Could not record the node link state: ${error instanceof Error ? error.message : String(error)}`
24937
+ )
24938
+ );
24939
+ });
24940
+ }
24941
+ }
22207
24942
  if (options.workerEnabled) {
22208
24943
  const jobClient = jobClientFor();
22209
24944
  const limits = clampResourceLimits(
@@ -22231,9 +24966,18 @@ function createNodeRuntime(config, io, options = {}) {
22231
24966
  ...io.monotonicNow ? { monotonicNow: io.monotonicNow } : {},
22232
24967
  // Agent computers: on an attended machine, live views belong to the
22233
24968
  // attended lane alone — never parked in (or holding a slot of) the
22234
- // work lane. Sent only under `--attend`, so a work-only node's lease
22235
- // body is exactly what it always was.
22236
- ...options.attendEnabled ? { excludeKinds: ["computer-session"] } : {},
24969
+ // work lane (only under `--attend`).
24970
+ //
24971
+ // Control 18: a machine whose owner has not allowed node-run shell
24972
+ // commands does not lease `acceptance-checks` jobs at all (every one
24973
+ // is a shell command in a directory the platform names), so they
24974
+ // stay with a node that will run them instead of failing here.
24975
+ ...(() => {
24976
+ const excluded = [];
24977
+ if (options.attendEnabled) excluded.push("computer-session");
24978
+ if (!executionPolicy.allowShellCommands) excluded.push("acceptance-checks");
24979
+ return excluded.length > 0 ? { excludeKinds: excluded } : {};
24980
+ })(),
22237
24981
  // Fleet usage limits: the CLIs an agent-task can run with here, so a
22238
24982
  // node whose only CLI is out of quota stops leasing agent-task work
22239
24983
  // until the reset (other kinds keep leasing).
@@ -22264,7 +25008,13 @@ function createNodeRuntime(config, io, options = {}) {
22264
25008
  ...io.diskProbe ? { diskProbe: io.diskProbe } : {},
22265
25009
  minFreeDiskBytes: effectiveMinFreeDiskBytes(limits)
22266
25010
  });
22267
- worker.register("acceptance-checks", (job, signal) => runAcceptanceChecksJob(job, {}, signal));
25011
+ worker.register(
25012
+ "acceptance-checks",
25013
+ (job, signal) => (
25014
+ // Control 18: refused here too, should one arrive despite the lease filter.
25015
+ runAcceptanceChecksJob(job, { executionPolicy }, signal)
25016
+ )
25017
+ );
22268
25018
  const modelCli = options.modelCli ?? io.environment.modelCli ?? {};
22269
25019
  worker.register("agent-task", async (job, signal, lease) => {
22270
25020
  const pushCredentials = lease ? new PushCredentialSession({
@@ -22371,6 +25121,11 @@ function createNodeRuntime(config, io, options = {}) {
22371
25121
  // read from the wire: a job cannot talk a node into
22372
25122
  // believing it is another one.
22373
25123
  nodeId: config.nodeId,
25124
+ // Control 18 — the machine owner's local execution policy,
25125
+ // from the LOCAL config only (OFF/OFF unless the owner ran
25126
+ // `ever-works-node policy`). Always passed: an executor that
25127
+ // gets no policy enforces none.
25128
+ executionPolicy,
22374
25129
  // Self-build slice Z (EW-796) — the platform side of the
22375
25130
  // MCP bridge, wired through the SAME authenticated job
22376
25131
  // client the lease protocol uses. No new endpoint, no new
@@ -22535,7 +25290,7 @@ function installShutdownHandlers(signals, shutdown) {
22535
25290
  __name(installShutdownHandlers, "installShutdownHandlers");
22536
25291
 
22537
25292
  // src/core/worker-safety-store.ts
22538
- var import_node_crypto3 = require("node:crypto");
25293
+ var import_node_crypto4 = require("node:crypto");
22539
25294
  var MARKER_VERSION = 1;
22540
25295
  var WorkerSafetyPrerequisiteError = class extends Error {
22541
25296
  static {
@@ -22553,7 +25308,7 @@ __name(workerSafetyMarkerPath, "workerSafetyMarkerPath");
22553
25308
  function createConfigWorkerSafetyGate(fs10, configPath, options = {}) {
22554
25309
  const markerPath = workerSafetyMarkerPath(configPath);
22555
25310
  const now = options.now ?? (() => Date.now());
22556
- const createSessionId = options.createSessionId ?? import_node_crypto3.randomUUID;
25311
+ const createSessionId = options.createSessionId ?? import_node_crypto4.randomUUID;
22557
25312
  const readRaw = /* @__PURE__ */ __name(async () => {
22558
25313
  try {
22559
25314
  return await fs10.readFile(markerPath);
@@ -22664,7 +25419,7 @@ function detail(error) {
22664
25419
  __name(detail, "detail");
22665
25420
 
22666
25421
  // src/core/workspaces/workspace-inventory.ts
22667
- var import_node_crypto4 = require("node:crypto");
25422
+ var import_node_crypto5 = require("node:crypto");
22668
25423
  var import_node_fs5 = require("node:fs");
22669
25424
  var import_node_path5 = require("node:path");
22670
25425
  var FLEET_WORKSPACE_STAMP_FILE = "ew-workspace.json";
@@ -23091,7 +25846,7 @@ async function readStamp(gitDir) {
23091
25846
  }
23092
25847
  __name(readStamp, "readStamp");
23093
25848
  function intentPath(poolPath, bindingKey) {
23094
- const key = (0, import_node_crypto4.createHash)("sha256").update("workspace-intent-v1\0").update(bindingKey).digest("hex");
25849
+ const key = (0, import_node_crypto5.createHash)("sha256").update("workspace-intent-v1\0").update(bindingKey).digest("hex");
23095
25850
  return (0, import_node_path5.join)(poolPath, FLEET_WORKSPACE_INTENTS_DIR, `${key}.json`);
23096
25851
  }
23097
25852
  __name(intentPath, "intentPath");
@@ -23842,6 +26597,7 @@ async function runStart(deps, options) {
23842
26597
  };
23843
26598
  const workerEnabled = options.work === true;
23844
26599
  const attendEnabled = options.attend === true;
26600
+ const manageEnabled = options.manage === true;
23845
26601
  const startPaused = config.paused === true;
23846
26602
  const concurrency = parseConcurrency(options.concurrency);
23847
26603
  const limitOverrides = parseLimitFlags(options);
@@ -23879,22 +26635,37 @@ async function runStart(deps, options) {
23879
26635
  // Written directly (not via a conditional spread) so excess-property
23880
26636
  // checking keeps this name tied to the runtime option it feeds.
23881
26637
  agentTaskWorkspaceRoot: workspaceRoot,
23882
- persistUnsafe: /* @__PURE__ */ __name(async (unsafe) => saveConfig(
23883
- deps.fs,
23884
- deps.configPath,
23885
- { ...config, unsafe },
23886
- {
23887
- platform: deps.platform,
26638
+ // Re-read before writing: the file may have changed since this service
26639
+ // started (`ever-works-node policy` run meanwhile), and a quarantine must
26640
+ // record `unsafe` without reverting anything else in it.
26641
+ persistUnsafe: /* @__PURE__ */ __name(async (unsafe) => {
26642
+ const current = await loadConfig(deps.fs, deps.configPath, {
23888
26643
  secrets: deps.secrets ?? null,
23889
26644
  logger: deps.io.logger
23890
- }
23891
- ), "persistUnsafe"),
26645
+ }).catch(() => null) ?? config;
26646
+ await saveConfig(
26647
+ deps.fs,
26648
+ deps.configPath,
26649
+ { ...current, unsafe },
26650
+ {
26651
+ platform: deps.platform,
26652
+ secrets: deps.secrets ?? null,
26653
+ logger: deps.io.logger
26654
+ }
26655
+ );
26656
+ }, "persistUnsafe"),
23892
26657
  workerSafetyGate: createConfigWorkerSafetyGate(deps.fs, deps.configPath, { platform: deps.platform }),
23893
26658
  // Node lifecycle (slice AR): what the platform last said about this
23894
26659
  // daemon's version, recorded beside the config for `status`/`doctor`.
23895
26660
  persistLifecycle: /* @__PURE__ */ __name((record) => writeNodeLifecycleRecord(deps.fs, deps.configPath, record), "persistLifecycle"),
23896
26661
  ...deps.resourceProbe ? { resourceProbe: deps.resourceProbe } : {},
23897
- attendEnabled
26662
+ attendEnabled,
26663
+ // Fleet node link (FL-02): the fourth consent, and the link itself. The
26664
+ // link opens on the first accepted heartbeat when any of --work,
26665
+ // --attend, --manage is on; its state is recorded for `status`.
26666
+ manageEnabled,
26667
+ nodeLink: true,
26668
+ persistLinkState: /* @__PURE__ */ __name((record) => writeNodeLinkRecord(deps.fs, deps.configPath, record), "persistLinkState")
23898
26669
  });
23899
26670
  if (workerEnabled) {
23900
26671
  void logModelCliCompatibility(deps, runtime.cliCompat, modelCli.paths);
@@ -23998,9 +26769,13 @@ async function runStart(deps, options) {
23998
26769
  if (!worker) {
23999
26770
  deps.out("Worker host disabled \u2014 reporting liveness only (pass --work to execute platform work)");
24000
26771
  }
26772
+ deps.out(
26773
+ isFleetLinkEligible(runtime.consents) ? `Consents \u2014 ${describeConsents(runtime.consents)}; the node link opens after the first accepted heartbeat` : "Consents \u2014 none: the node link stays closed and this computer only reports health (pass --work, --attend or --manage)"
26774
+ );
24001
26775
  await (deps.waitForShutdown?.() ?? (deps.signals ? signalled : Promise.resolve()));
24002
26776
  reaper?.stop();
24003
26777
  shuttingDown = true;
26778
+ runtime.link?.close();
24004
26779
  loop.stop();
24005
26780
  await loop.settled();
24006
26781
  await lanesStarted;
@@ -24048,6 +26823,61 @@ function parseConcurrency(raw) {
24048
26823
  return parsed;
24049
26824
  }
24050
26825
  __name(parseConcurrency, "parseConcurrency");
26826
+ function describeExecutionPolicy(policy) {
26827
+ return `skip-permissions ${policy.allowSkipPermissions ? "ALLOWED" : "refused"}, node-run shell commands ${policy.allowShellCommands ? "ALLOWED" : "refused"}, Claude Code restricted mode ${policy.restrictedMode ? "on" : "OFF"}`;
26828
+ }
26829
+ __name(describeExecutionPolicy, "describeExecutionPolicy");
26830
+ function parsePolicySwitch(flag, value) {
26831
+ if (value === void 0) return void 0;
26832
+ const normalized = value.trim().toLowerCase();
26833
+ if (normalized === "on" || normalized === "true" || normalized === "yes") return true;
26834
+ if (normalized === "off" || normalized === "false" || normalized === "no") return false;
26835
+ throw new CliError(`${flag} takes 'on' or 'off', not '${value}'`);
26836
+ }
26837
+ __name(parsePolicySwitch, "parsePolicySwitch");
26838
+ async function runPolicy(deps, options) {
26839
+ const config = await requireConfig(deps);
26840
+ const current = resolveNodeExecutionPolicy(config.executionPolicy);
26841
+ const skip = parsePolicySwitch("--allow-skip-permissions", options.allowSkipPermissions);
26842
+ const shell = parsePolicySwitch("--allow-shell-commands", options.allowShellCommands);
26843
+ const restricted = parsePolicySwitch("--restricted-mode", options.restrictedMode);
26844
+ if (skip === void 0 && shell === void 0 && restricted === void 0) {
26845
+ deps.out(`policy ${describeExecutionPolicy(current)}`);
26846
+ deps.out(
26847
+ "Change it with `ever-works-node policy --allow-skip-permissions on|off --allow-shell-commands on|off --restricted-mode on|off`."
26848
+ );
26849
+ return;
26850
+ }
26851
+ const next = {
26852
+ allowSkipPermissions: skip ?? current.allowSkipPermissions,
26853
+ allowShellCommands: shell ?? current.allowShellCommands,
26854
+ restrictedMode: restricted ?? current.restrictedMode
26855
+ };
26856
+ await saveConfig(
26857
+ deps.fs,
26858
+ deps.configPath,
26859
+ { ...config, executionPolicy: next },
26860
+ { platform: deps.platform, secrets: deps.secrets ?? null, logger: deps.io.logger }
26861
+ );
26862
+ deps.out(`policy ${describeExecutionPolicy(next)}`);
26863
+ if (next.allowSkipPermissions) {
26864
+ deps.out(
26865
+ "WARNING: jobs may now run the model CLI with its permission checks OFF. A prompt injection in a Task, a repository or a review comment then has an unrestricted shell as this account, with no filesystem or network boundary."
26866
+ );
26867
+ }
26868
+ if (next.allowShellCommands) {
26869
+ deps.out(
26870
+ "WARNING: jobs may now make this node run shell commands (setup, acceptance checks). They execute repository code as this account \u2014 after a model step, code the model may have written."
26871
+ );
26872
+ }
26873
+ if (!next.restrictedMode) {
26874
+ deps.out(
26875
+ "Note: Claude Code restricted mode is OFF \u2014 runs keep the baseline isolation (no repository settings, no hooks) but get the command-running built-ins back and read your ~/.claude/settings.json."
26876
+ );
26877
+ }
26878
+ deps.out("Restart the node service for a running worker to apply the change.");
26879
+ }
26880
+ __name(runPolicy, "runPolicy");
24051
26881
  async function runPause(deps, paused, options = {}) {
24052
26882
  const config = await requireConfig(deps);
24053
26883
  const verb = paused ? "Paused" : "Resumed";
@@ -24195,12 +27025,18 @@ async function runStatus(deps) {
24195
27025
  deps.out(`work ${view.paused ? "PAUSED (draining \u2014 no new work)" : "accepting new work"}`);
24196
27026
  const lifecycleRecord = await readNodeLifecycleRecord(deps.fs, deps.configPath);
24197
27027
  deps.out(`daemon ${describeDaemonLifecycle(deps, lifecycleRecord)}`);
27028
+ const linkRecord = await readNodeLinkRecord(deps.fs, deps.configPath);
27029
+ deps.out(
27030
+ `consents ${linkRecord ? describeConsents(linkRecord.consents) : "unknown (the service has not reported them yet)"}`
27031
+ );
27032
+ deps.out(`link ${describeLinkRecord(linkRecord, (deps.now ?? (() => Date.now()))())}`);
24198
27033
  deps.out(
24199
27034
  `offering ${view.capabilitySelection ? view.capabilitySelection.join(", ") || "(identity only)" : "(everything detected)"}`
24200
27035
  );
24201
27036
  deps.out(`limits ${describeLimits(view.limits)}`);
24202
27037
  deps.out(`ceiling ${describePlatformCeiling(lifecycleRecord, view.limits)}`);
24203
27038
  deps.out(`workspace gc ${describeWorkspaceGc(view.workspaceGc ?? DEFAULT_WORKSPACE_GC_POLICY)}`);
27039
+ deps.out(`policy ${describeExecutionPolicy(view.executionPolicy)}`);
24204
27040
  deps.out(`credential ${view.hasSecret ? "stored" : "MISSING"}`);
24205
27041
  deps.out(`config ${deps.configPath}`);
24206
27042
  }
@@ -24407,6 +27243,11 @@ function describeModelCli(compat) {
24407
27243
  if (compat.missingConditionalFlags.length > 0) {
24408
27244
  notes.push(`runs that need ${compat.missingConditionalFlags.join(", ")} will fail here`);
24409
27245
  }
27246
+ if ((compat.missingHardeningFlags ?? []).length > 0) {
27247
+ notes.push(
27248
+ `runs go without the stricter ${compat.missingHardeningFlags.join(", ")} mode (upgrade the CLI to get it)`
27249
+ );
27250
+ }
24410
27251
  return `${head} \u2014 compatible${notes.length > 0 ? `; ${notes.join("; ")}` : ""}`;
24411
27252
  }
24412
27253
  __name(describeModelCli, "describeModelCli");
@@ -24474,6 +27315,7 @@ function lifecycleJson(deps, report) {
24474
27315
  missingRequiredFlags: compat.missingRequiredFlags,
24475
27316
  missingConditionalFlags: compat.missingConditionalFlags,
24476
27317
  unsupportedOptionalFlags: compat.unsupportedOptionalFlags,
27318
+ missingHardeningFlags: compat.missingHardeningFlags ?? [],
24477
27319
  note: compat.note
24478
27320
  }))
24479
27321
  };
@@ -24502,6 +27344,11 @@ async function runDoctor(deps, options) {
24502
27344
  }
24503
27345
  printHousekeepingHeader(deps, report, now);
24504
27346
  printLifecycle(deps, lifecycle);
27347
+ const linkRecord = await readNodeLinkRecord(deps.fs, deps.configPath);
27348
+ deps.out(
27349
+ `consents ${linkRecord ? describeConsents(linkRecord.consents) : "unknown (the service has not reported them yet)"}`
27350
+ );
27351
+ deps.out(`link ${describeLinkRecord(linkRecord, now)}`);
24505
27352
  printPlan(deps, report.plan, now);
24506
27353
  }
24507
27354
  __name(runDoctor, "runDoctor");
@@ -24588,6 +27435,9 @@ function buildProgram(deps) {
24588
27435
  program2.command("start").description("Run the heartbeat loop (and, with --work, the job worker host) until SIGINT/SIGTERM").option("-i, --heartbeat-interval <seconds>", "Override the stored heartbeat cadence, in seconds").option("-w, --work", "Lease and execute platform work on this machine (off by default)").option(
24589
27436
  "--attend",
24590
27437
  "Allow live viewing of this machine from the dashboard (off by default; independent of --work)"
27438
+ ).option(
27439
+ "--manage",
27440
+ "Allow managing this machine from the dashboard \u2014 owner sessions, logs, services, Docker, processes, ports, files, commands, schedules (off by default; independent of --work and --attend)"
24591
27441
  ).option(
24592
27442
  "-c, --concurrency <count>",
24593
27443
  "Max jobs to execute at once when --work is set (overrides the stored limit)"
@@ -24612,6 +27462,20 @@ function buildProgram(deps) {
24612
27462
  program2.command("resume").description("Take work again after a pause").option("--local-only", "Only clear the local flag; do not tell the platform").action(async (options) => {
24613
27463
  await runPause(deps, false, options);
24614
27464
  });
27465
+ program2.command("policy").description(
27466
+ "Show or change what platform jobs may make this machine do (local only; the platform cannot change it)"
27467
+ ).option(
27468
+ "--allow-skip-permissions <on|off>",
27469
+ "Let jobs run the model CLI with its permission checks off (--dangerously-skip-permissions). Default off."
27470
+ ).option(
27471
+ "--allow-shell-commands <on|off>",
27472
+ "Let jobs make this node run shell commands: setup, command steps, acceptance checks. Default off."
27473
+ ).option(
27474
+ "--restricted-mode <on|off>",
27475
+ "Run Claude Code with --restricted when it supports it (no shell tools, no settings files). Default on."
27476
+ ).action(async (options) => {
27477
+ await runPolicy(deps, options);
27478
+ });
24615
27479
  program2.command("clear-quarantine").description("Clear a persisted unsafe worker state after verifying every prior process tree is stopped").option(
24616
27480
  "--confirm-process-tree-stopped",
24617
27481
  "Assert that the prior Git/command process tree has been independently verified stopped"
@@ -24705,15 +27569,16 @@ function createLogger(options = {}) {
24705
27569
  __name(createLogger, "createLogger");
24706
27570
 
24707
27571
  // src/version.ts
24708
- var NODE_APP_VERSION = "0.2.5";
27572
+ var NODE_APP_VERSION = "0.2.6";
24709
27573
 
24710
27574
  // src/cli.ts
24711
27575
  function buildDeps() {
24712
27576
  const logger = createLogger();
27577
+ const runner = createCommandRunner();
24713
27578
  return {
24714
27579
  io: {
24715
27580
  fetchFn: systemFetch,
24716
- runner: createCommandRunner(),
27581
+ runner,
24717
27582
  environment: currentEnvironment(),
24718
27583
  logger,
24719
27584
  version: NODE_APP_VERSION,
@@ -24724,7 +27589,9 @@ function buildDeps() {
24724
27589
  diskProbe: createDiskProbe(),
24725
27590
  // Node lifecycle (slice AR): the cache key of the model-CLI
24726
27591
  // compatibility probe, so an upgraded CLI is re-probed at once.
24727
- statFile: statModelCliFile
27592
+ statFile: statModelCliFile,
27593
+ // Computer health (FL-03): the snapshot every heartbeat carries.
27594
+ health: createHealthProbeIo(runner)
24728
27595
  },
24729
27596
  fs: createConfigFileSystem(),
24730
27597
  configPath: defaultConfigPath(),