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.
- package/README.md +8 -0
- package/cli.js +3048 -181
- 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.
|
|
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.
|
|
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 =
|
|
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(
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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/
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
|
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
|
|
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 (!
|
|
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/
|
|
22055
|
-
|
|
22056
|
-
|
|
22057
|
-
|
|
22058
|
-
|
|
22059
|
-
|
|
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
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
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
|
-
|
|
22123
|
-
|
|
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
|
-
|
|
22139
|
-
|
|
22140
|
-
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
|
|
22144
|
-
|
|
22145
|
-
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
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
|
|
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
|
-
|
|
22165
|
-
|
|
22166
|
-
|
|
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(() =>
|
|
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
|
|
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
|
|
22235
|
-
//
|
|
22236
|
-
|
|
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(
|
|
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
|
|
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 ??
|
|
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
|
|
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,
|
|
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
|
-
|
|
23883
|
-
|
|
23884
|
-
|
|
23885
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(),
|