@agentconnect.md/daemon 1.0.0-rc.61 → 1.0.0-rc.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +420 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { EventEmitter } from "node:events";
|
|
|
4
4
|
import childProcess, { execFile, spawn } from "node:child_process";
|
|
5
5
|
import * as sp from "node:path";
|
|
6
6
|
import path, { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
7
|
-
import fs, { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, stat, unlinkSync, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
|
|
7
|
+
import fs, { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, promises, readFileSync, readdirSync, rmSync, stat, unlinkSync, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
|
|
8
8
|
import process$1 from "node:process";
|
|
9
9
|
import { stripVTControlCharacters } from "node:util";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -7595,7 +7595,7 @@ function cachedRuntimeNames(root) {
|
|
|
7595
7595
|
}
|
|
7596
7596
|
/**
|
|
7597
7597
|
* Runtime versions (registry id -> `version`) read from the local cache. Used to
|
|
7598
|
-
* stamp the `facts/
|
|
7598
|
+
* stamp each entry of the `facts/daemon-runtimes` snapshot the daemon reports.
|
|
7599
7599
|
* Entries with no `version` are omitted (callers fall back to ''). `{}` when no
|
|
7600
7600
|
* cached registry doc exists.
|
|
7601
7601
|
*/
|
|
@@ -13254,6 +13254,140 @@ const SessionHistoryPage = object({
|
|
|
13254
13254
|
nextCursor: string().optional()
|
|
13255
13255
|
});
|
|
13256
13256
|
//#endregion
|
|
13257
|
+
//#region ../protocol/dist/frames/workspace.js
|
|
13258
|
+
/**
|
|
13259
|
+
* Agent workspace file browsing (C→D REQ → REP) — the console's on-demand pulls.
|
|
13260
|
+
*
|
|
13261
|
+
* The CP stores NO workspace data — directory listings and file bytes are pulled
|
|
13262
|
+
* live from the owning daemon and proxied to the console, never persisted
|
|
13263
|
+
* (body-locality). Missing paths are DATA (`exists:false`), not errors; only
|
|
13264
|
+
* path-containment violations (`BAD_PAYLOAD`) and unexpected fs failures
|
|
13265
|
+
* (`INTERNAL`) come back as `error` frames.
|
|
13266
|
+
*
|
|
13267
|
+
* - `workspace/list`: one cursor-paginated page of a directory's entries.
|
|
13268
|
+
* - `workspace/read`: one byte slice of a file. `limit` is only a ceiling — the
|
|
13269
|
+
* daemon returns FEWER bytes when needed to keep the JSON-escaped REP under the
|
|
13270
|
+
* 256 KiB frame cap (control bytes escape to `\uXXXX`, a 6× blowup), and always
|
|
13271
|
+
* ends the slice on a UTF-8 character boundary. `nextOffset` is the authoritative
|
|
13272
|
+
* byte offset to request next — callers must NOT recompute it from the decoded
|
|
13273
|
+
* `content` (a split multi-byte char would make the byte count drift).
|
|
13274
|
+
*/
|
|
13275
|
+
/** One directory entry in a workspace listing (name-only; not a full path). */
|
|
13276
|
+
const WorkspaceEntry = object({
|
|
13277
|
+
name: string(),
|
|
13278
|
+
type: _enum([
|
|
13279
|
+
"dir",
|
|
13280
|
+
"file",
|
|
13281
|
+
"symlink",
|
|
13282
|
+
"other"
|
|
13283
|
+
]),
|
|
13284
|
+
size: number().int().nonnegative().optional(),
|
|
13285
|
+
mtime: string().optional()
|
|
13286
|
+
});
|
|
13287
|
+
/** C→D REQ: list one page of a directory in the agent's workspace. */
|
|
13288
|
+
const WorkspaceListReq = object({
|
|
13289
|
+
agentId: string().min(1),
|
|
13290
|
+
path: string().default(""),
|
|
13291
|
+
cursor: string().optional(),
|
|
13292
|
+
limit: number().int().positive().max(500).default(200)
|
|
13293
|
+
});
|
|
13294
|
+
/** D→C REP (corr = the req id): a page of entries + the cursor for the next page. */
|
|
13295
|
+
const WorkspaceListPage = object({
|
|
13296
|
+
agentId: string(),
|
|
13297
|
+
path: string(),
|
|
13298
|
+
exists: boolean(),
|
|
13299
|
+
entries: array(WorkspaceEntry),
|
|
13300
|
+
nextCursor: string().optional()
|
|
13301
|
+
});
|
|
13302
|
+
/** C→D REQ: read one byte slice of a file in the agent's workspace. */
|
|
13303
|
+
const WorkspaceReadReq = object({
|
|
13304
|
+
agentId: string().min(1),
|
|
13305
|
+
path: string().min(1),
|
|
13306
|
+
offset: number().int().nonnegative().default(0),
|
|
13307
|
+
limit: number().int().positive().max(65536).default(65536)
|
|
13308
|
+
});
|
|
13309
|
+
/** D→C REP (corr = the req id): the file slice (or `exists:false` / binary-detected). */
|
|
13310
|
+
const WorkspaceReadContent = object({
|
|
13311
|
+
agentId: string(),
|
|
13312
|
+
path: string(),
|
|
13313
|
+
exists: boolean(),
|
|
13314
|
+
size: number().int().nonnegative().optional(),
|
|
13315
|
+
mtime: string().optional(),
|
|
13316
|
+
encoding: _enum(["utf8", "none"]).optional(),
|
|
13317
|
+
content: string().optional(),
|
|
13318
|
+
offset: number().int().nonnegative().optional(),
|
|
13319
|
+
nextOffset: number().int().nonnegative().optional(),
|
|
13320
|
+
truncated: boolean().optional()
|
|
13321
|
+
});
|
|
13322
|
+
//#endregion
|
|
13323
|
+
//#region ../protocol/dist/frames/telemetry.js
|
|
13324
|
+
/**
|
|
13325
|
+
* Telemetry & facts (D→C) — protocol §7.
|
|
13326
|
+
*
|
|
13327
|
+
* The dashboard's real-time source. `event/session` is the converged milestone
|
|
13328
|
+
* feed (NOT the message stream); `facts/daemon-runtimes` is the observed runtime
|
|
13329
|
+
* capability feed; `heartbeat` drives the watchdog.
|
|
13330
|
+
*/
|
|
13331
|
+
const Heartbeat = object({
|
|
13332
|
+
load: object({
|
|
13333
|
+
cpu: number(),
|
|
13334
|
+
mem: number(),
|
|
13335
|
+
agents: number().int()
|
|
13336
|
+
}),
|
|
13337
|
+
health: _enum(["ok", "degraded"]),
|
|
13338
|
+
activeSessions: number().int(),
|
|
13339
|
+
degradedScopes: array(string()).default([])
|
|
13340
|
+
});
|
|
13341
|
+
/** Converged session lifecycle milestone — protocol §7.2. NOT the message stream. */
|
|
13342
|
+
const EventSession = object({
|
|
13343
|
+
sessionId: string().uuid(),
|
|
13344
|
+
agentId: string().uuid(),
|
|
13345
|
+
launchId: string().uuid(),
|
|
13346
|
+
phase: _enum([
|
|
13347
|
+
"start",
|
|
13348
|
+
"plan",
|
|
13349
|
+
"problem",
|
|
13350
|
+
"end"
|
|
13351
|
+
]),
|
|
13352
|
+
link: string().optional(),
|
|
13353
|
+
summary: string().optional(),
|
|
13354
|
+
ts: string().datetime()
|
|
13355
|
+
});
|
|
13356
|
+
/**
|
|
13357
|
+
* Per-session token-usage report — D→C EVT. The daemon meters usage from the
|
|
13358
|
+
* agent's ACP stream and reports the session's CUMULATIVE snapshot (latest-wins)
|
|
13359
|
+
* so the CP can persist it for the console's historical usage aggregates. This is
|
|
13360
|
+
* dashboard metadata (token counts + cost), never the message stream. Idempotent
|
|
13361
|
+
* by `sessionId`: re-sending the same snapshot is a no-op upsert on the CP.
|
|
13362
|
+
*/
|
|
13363
|
+
const UsageReport = object({
|
|
13364
|
+
sessionId: string(),
|
|
13365
|
+
agentId: string().uuid(),
|
|
13366
|
+
platform: string().optional(),
|
|
13367
|
+
channel: string().optional(),
|
|
13368
|
+
lastActivityAt: string(),
|
|
13369
|
+
usage: SessionUsage
|
|
13370
|
+
});
|
|
13371
|
+
/**
|
|
13372
|
+
* Observed capabilities of an installed runtime — protocol §7.3. The element
|
|
13373
|
+
* type of the `facts/daemon-runtimes` snapshot. As a standalone per-runtime
|
|
13374
|
+
* frame (`facts/runtime-profile`, upsert-only) it is DEPRECATED: current
|
|
13375
|
+
* daemons report the full snapshot instead; the CP keeps accepting the old
|
|
13376
|
+
* frame from older daemons.
|
|
13377
|
+
*/
|
|
13378
|
+
const FactsRuntimeProfile = object({
|
|
13379
|
+
runtime: string(),
|
|
13380
|
+
version: string(),
|
|
13381
|
+
models: array(string()),
|
|
13382
|
+
contextWindow: number().int().optional(),
|
|
13383
|
+
acpSupport: _enum([
|
|
13384
|
+
"full",
|
|
13385
|
+
"partial",
|
|
13386
|
+
"none"
|
|
13387
|
+
]),
|
|
13388
|
+
toolCalling: boolean()
|
|
13389
|
+
});
|
|
13390
|
+
//#endregion
|
|
13257
13391
|
//#region ../protocol/dist/frame.js
|
|
13258
13392
|
/**
|
|
13259
13393
|
* The single source of truth for the wire: `type` string → payload zod schema.
|
|
@@ -13271,16 +13405,7 @@ const FRAME_SCHEMAS = {
|
|
|
13271
13405
|
"auth/ok": AuthOk,
|
|
13272
13406
|
register: RegisterReq,
|
|
13273
13407
|
"register/ok": RegisterOk,
|
|
13274
|
-
heartbeat:
|
|
13275
|
-
load: object({
|
|
13276
|
-
cpu: number(),
|
|
13277
|
-
mem: number(),
|
|
13278
|
-
agents: number().int()
|
|
13279
|
-
}),
|
|
13280
|
-
health: _enum(["ok", "degraded"]),
|
|
13281
|
-
activeSessions: number().int(),
|
|
13282
|
-
degradedScopes: array(string()).default([])
|
|
13283
|
-
}),
|
|
13408
|
+
heartbeat: Heartbeat,
|
|
13284
13409
|
"agent/launch": AgentLaunch,
|
|
13285
13410
|
"agent/launched": AgentLaunched,
|
|
13286
13411
|
"agent/stop": AgentStop,
|
|
@@ -13306,44 +13431,18 @@ const FRAME_SCHEMAS = {
|
|
|
13306
13431
|
"secrets/renew": SecretsRenew,
|
|
13307
13432
|
"secrets/revoke": SecretsRevoke,
|
|
13308
13433
|
"scope-attestation": ScopeAttestation,
|
|
13309
|
-
"event/session":
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
phase: _enum([
|
|
13314
|
-
"start",
|
|
13315
|
-
"plan",
|
|
13316
|
-
"problem",
|
|
13317
|
-
"end"
|
|
13318
|
-
]),
|
|
13319
|
-
link: string().optional(),
|
|
13320
|
-
summary: string().optional(),
|
|
13321
|
-
ts: string().datetime()
|
|
13322
|
-
}),
|
|
13323
|
-
"usage/report": object({
|
|
13324
|
-
sessionId: string(),
|
|
13325
|
-
agentId: string().uuid(),
|
|
13326
|
-
platform: string().optional(),
|
|
13327
|
-
channel: string().optional(),
|
|
13328
|
-
lastActivityAt: string(),
|
|
13329
|
-
usage: SessionUsage
|
|
13330
|
-
}),
|
|
13331
|
-
"facts/runtime-profile": object({
|
|
13332
|
-
runtime: string(),
|
|
13333
|
-
version: string(),
|
|
13334
|
-
models: array(string()),
|
|
13335
|
-
contextWindow: number().int().optional(),
|
|
13336
|
-
acpSupport: _enum([
|
|
13337
|
-
"full",
|
|
13338
|
-
"partial",
|
|
13339
|
-
"none"
|
|
13340
|
-
]),
|
|
13341
|
-
toolCalling: boolean()
|
|
13342
|
-
}),
|
|
13434
|
+
"event/session": EventSession,
|
|
13435
|
+
"usage/report": UsageReport,
|
|
13436
|
+
"facts/runtime-profile": FactsRuntimeProfile,
|
|
13437
|
+
"facts/daemon-runtimes": object({ runtimes: array(FactsRuntimeProfile) }),
|
|
13343
13438
|
"session/list": SessionListReq,
|
|
13344
13439
|
"session/list/page": SessionListPage,
|
|
13345
13440
|
"session/history": SessionHistoryReq,
|
|
13346
13441
|
"session/history/page": SessionHistoryPage,
|
|
13442
|
+
"workspace/list": WorkspaceListReq,
|
|
13443
|
+
"workspace/list/page": WorkspaceListPage,
|
|
13444
|
+
"workspace/read": WorkspaceReadReq,
|
|
13445
|
+
"workspace/read/content": WorkspaceReadContent,
|
|
13347
13446
|
"config/push": object({ keys: record(string(), unknown()) }),
|
|
13348
13447
|
"daemon/restart": object({
|
|
13349
13448
|
reason: string(),
|
|
@@ -13434,10 +13533,15 @@ discriminatedUnion("type", [
|
|
|
13434
13533
|
frame("event/session", FRAME_SCHEMAS["event/session"]),
|
|
13435
13534
|
frame("usage/report", FRAME_SCHEMAS["usage/report"]),
|
|
13436
13535
|
frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
|
|
13536
|
+
frame("facts/daemon-runtimes", FRAME_SCHEMAS["facts/daemon-runtimes"]),
|
|
13437
13537
|
frame("session/list", FRAME_SCHEMAS["session/list"]),
|
|
13438
13538
|
frame("session/list/page", FRAME_SCHEMAS["session/list/page"]),
|
|
13439
13539
|
frame("session/history", FRAME_SCHEMAS["session/history"]),
|
|
13440
13540
|
frame("session/history/page", FRAME_SCHEMAS["session/history/page"]),
|
|
13541
|
+
frame("workspace/list", FRAME_SCHEMAS["workspace/list"]),
|
|
13542
|
+
frame("workspace/list/page", FRAME_SCHEMAS["workspace/list/page"]),
|
|
13543
|
+
frame("workspace/read", FRAME_SCHEMAS["workspace/read"]),
|
|
13544
|
+
frame("workspace/read/content", FRAME_SCHEMAS["workspace/read/content"]),
|
|
13441
13545
|
frame("config/push", FRAME_SCHEMAS["config/push"]),
|
|
13442
13546
|
frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
|
|
13443
13547
|
frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
|
|
@@ -81880,13 +81984,14 @@ async function probeRuntime(id, rt, cwd, opts = {}) {
|
|
|
81880
81984
|
return host.modelOptions();
|
|
81881
81985
|
};
|
|
81882
81986
|
try {
|
|
81883
|
-
const
|
|
81884
|
-
|
|
81987
|
+
const opt = await Promise.race([run(), timeout]);
|
|
81988
|
+
const models = (opt?.models ?? []).filter((m) => m !== "default");
|
|
81989
|
+
opts.log?.info(`probe: ${id} ok (models: ${models.length ? models.join(", ") : "none advertised"})`);
|
|
81885
81990
|
return {
|
|
81886
81991
|
runtime: id,
|
|
81887
81992
|
ok: true,
|
|
81888
|
-
models
|
|
81889
|
-
currentModel:
|
|
81993
|
+
models,
|
|
81994
|
+
currentModel: opt?.current
|
|
81890
81995
|
};
|
|
81891
81996
|
} catch (err) {
|
|
81892
81997
|
const error = err.message;
|
|
@@ -81960,6 +82065,237 @@ function makeLogger(level = "info") {
|
|
|
81960
82065
|
};
|
|
81961
82066
|
}
|
|
81962
82067
|
//#endregion
|
|
82068
|
+
//#region src/cp/workspace-reader.ts
|
|
82069
|
+
/**
|
|
82070
|
+
* `WorkspaceReader` — the read-only seam answering the CP's `workspace/list` and
|
|
82071
|
+
* `workspace/read` REQs from the agent's local workspace directory. File bytes
|
|
82072
|
+
* live only on the daemon (§1/§12); this streams single pages/slices to the CP,
|
|
82073
|
+
* never whole trees or files.
|
|
82074
|
+
*
|
|
82075
|
+
* Containment is to `workspace.path` EXACTLY — the parent directory holds
|
|
82076
|
+
* `agent.json` and `.env` secrets, so escaping even one level is a secret leak.
|
|
82077
|
+
* Every request path is (1) rejected if absolute, (2) lexically resolved against
|
|
82078
|
+
* the root and prefix-checked, then (3) canonicalised with `realpath` and
|
|
82079
|
+
* re-verified so symlinks (including an intermediate directory component swapped
|
|
82080
|
+
* to a symlink after the check) cannot smuggle a target outside the root; all
|
|
82081
|
+
* I/O runs on the canonical path. Violations throw `WorkspaceViolationError`,
|
|
82082
|
+
* which the dispatcher maps to a `BAD_PAYLOAD` error frame; missing paths are
|
|
82083
|
+
* data (`exists:false`), not errors. Never log file contents.
|
|
82084
|
+
*
|
|
82085
|
+
* Frame-size safety: every REP must serialise under the 256 KiB wire cap. JSON
|
|
82086
|
+
* escaping of control bytes is a 6× blowup, so `read` bounds the slice by the
|
|
82087
|
+
* *encoded* reply size (not the raw byte `limit`) and `list` stops a page early
|
|
82088
|
+
* when the encoded entries would overflow — both leave headroom for the envelope.
|
|
82089
|
+
*/
|
|
82090
|
+
/** Bytes sniffed from the head of a file for binary (NUL byte) detection. */
|
|
82091
|
+
const SNIFF_BYTES = 8192;
|
|
82092
|
+
/** Encoded-payload ceiling, leaving headroom under MAX_FRAME_BYTES for the
|
|
82093
|
+
* envelope (id/ts/type/corr + fencing ext, well under 4 KiB). */
|
|
82094
|
+
const REPLY_BUDGET = MAX_FRAME_BYTES - 4096;
|
|
82095
|
+
/** Path-containment / bad-request violation → `BAD_PAYLOAD` on the wire. */
|
|
82096
|
+
var WorkspaceViolationError = class extends Error {
|
|
82097
|
+
constructor(message) {
|
|
82098
|
+
super(message);
|
|
82099
|
+
this.name = "WorkspaceViolationError";
|
|
82100
|
+
}
|
|
82101
|
+
};
|
|
82102
|
+
function isErrno(err, code) {
|
|
82103
|
+
return err?.code === code;
|
|
82104
|
+
}
|
|
82105
|
+
function under(root, p) {
|
|
82106
|
+
return p === root || p.startsWith(root + sp.sep);
|
|
82107
|
+
}
|
|
82108
|
+
/** Largest index ≤ `len` that lands on a UTF-8 character boundary — i.e. never
|
|
82109
|
+
* cuts a multi-byte sequence. If the slice's trailing char is incomplete, the
|
|
82110
|
+
* boundary is placed before that char (its bytes belong to the next slice). */
|
|
82111
|
+
function utf8Boundary(buf, len) {
|
|
82112
|
+
if (len >= buf.length) len = buf.length;
|
|
82113
|
+
if (len <= 0) return 0;
|
|
82114
|
+
let start = len - 1;
|
|
82115
|
+
while (start > 0 && ((buf[start] ?? 0) & 192) === 128) start--;
|
|
82116
|
+
const lead = buf[start] ?? 0;
|
|
82117
|
+
const seqLen = lead < 128 ? 1 : (lead & 224) === 192 ? 2 : (lead & 240) === 224 ? 3 : (lead & 248) === 240 ? 4 : 1;
|
|
82118
|
+
return start + seqLen <= len ? len : start;
|
|
82119
|
+
}
|
|
82120
|
+
/** The encoded size of the payload the wire will carry (JSON.stringify matches
|
|
82121
|
+
* the codec's `encode`), measured in bytes. */
|
|
82122
|
+
function encodedBytes(payload) {
|
|
82123
|
+
return Buffer.byteLength(JSON.stringify(payload));
|
|
82124
|
+
}
|
|
82125
|
+
function createWorkspaceReader(workspaceRootByAgent) {
|
|
82126
|
+
function rootFor(agentId) {
|
|
82127
|
+
const root = workspaceRootByAgent(agentId);
|
|
82128
|
+
if (!root) throw new WorkspaceViolationError(`unknown agent "${agentId}"`);
|
|
82129
|
+
return root;
|
|
82130
|
+
}
|
|
82131
|
+
/**
|
|
82132
|
+
* Lexical containment. Rejects absolute paths and `..` escapes; returns the
|
|
82133
|
+
* lexically-resolved path plus the canonical (realpath'd) root, or `null` for
|
|
82134
|
+
* `realRoot` when the root does not exist yet (callers report `exists:false`).
|
|
82135
|
+
*/
|
|
82136
|
+
async function resolveContained(root, relPath) {
|
|
82137
|
+
if (sp.isAbsolute(relPath)) throw new WorkspaceViolationError("absolute paths are not allowed");
|
|
82138
|
+
const resolved = sp.resolve(root, relPath);
|
|
82139
|
+
if (!under(root, resolved)) throw new WorkspaceViolationError("path escapes the workspace root");
|
|
82140
|
+
try {
|
|
82141
|
+
return {
|
|
82142
|
+
resolved,
|
|
82143
|
+
realRoot: await promises.realpath(root)
|
|
82144
|
+
};
|
|
82145
|
+
} catch (err) {
|
|
82146
|
+
if (isErrno(err, "ENOENT")) return {
|
|
82147
|
+
resolved,
|
|
82148
|
+
realRoot: null
|
|
82149
|
+
};
|
|
82150
|
+
throw err;
|
|
82151
|
+
}
|
|
82152
|
+
}
|
|
82153
|
+
/** Canonicalise an existing target and re-verify containment on the REAL path,
|
|
82154
|
+
* closing the check-vs-use gap (a symlinked intermediate component resolves
|
|
82155
|
+
* out and is rejected here). Returns the canonical path used for I/O. */
|
|
82156
|
+
async function canonicalUnder(realRoot, abs) {
|
|
82157
|
+
const canon = await promises.realpath(abs);
|
|
82158
|
+
if (!under(realRoot, canon)) throw new WorkspaceViolationError("path resolves outside the workspace root");
|
|
82159
|
+
return canon;
|
|
82160
|
+
}
|
|
82161
|
+
return {
|
|
82162
|
+
async list(req) {
|
|
82163
|
+
const { resolved, realRoot } = await resolveContained(rootFor(req.agentId), req.path);
|
|
82164
|
+
const notFound = {
|
|
82165
|
+
agentId: req.agentId,
|
|
82166
|
+
path: req.path,
|
|
82167
|
+
exists: false,
|
|
82168
|
+
entries: []
|
|
82169
|
+
};
|
|
82170
|
+
if (realRoot === null) return notFound;
|
|
82171
|
+
let target;
|
|
82172
|
+
try {
|
|
82173
|
+
target = await canonicalUnder(realRoot, resolved);
|
|
82174
|
+
} catch (err) {
|
|
82175
|
+
if (isErrno(err, "ENOENT")) return notFound;
|
|
82176
|
+
throw err;
|
|
82177
|
+
}
|
|
82178
|
+
let dirents;
|
|
82179
|
+
try {
|
|
82180
|
+
dirents = await promises.readdir(target, { withFileTypes: true });
|
|
82181
|
+
} catch (err) {
|
|
82182
|
+
if (isErrno(err, "ENOENT") || isErrno(err, "ENOTDIR")) return notFound;
|
|
82183
|
+
throw err;
|
|
82184
|
+
}
|
|
82185
|
+
const entries = [];
|
|
82186
|
+
for (const d of dirents) {
|
|
82187
|
+
if (d.name === ".git") continue;
|
|
82188
|
+
const type = d.isDirectory() ? "dir" : d.isFile() ? "file" : d.isSymbolicLink() ? "symlink" : "other";
|
|
82189
|
+
const entry = {
|
|
82190
|
+
name: d.name,
|
|
82191
|
+
type
|
|
82192
|
+
};
|
|
82193
|
+
if (type === "file") try {
|
|
82194
|
+
const st = await promises.lstat(sp.join(target, d.name));
|
|
82195
|
+
entry.size = st.size;
|
|
82196
|
+
entry.mtime = st.mtime.toISOString();
|
|
82197
|
+
} catch {}
|
|
82198
|
+
entries.push(entry);
|
|
82199
|
+
}
|
|
82200
|
+
entries.sort(compareEntries);
|
|
82201
|
+
const start = req.cursor !== void 0 && /^\d+$/.test(req.cursor) ? Number(req.cursor) : 0;
|
|
82202
|
+
const page = [];
|
|
82203
|
+
let acc = 0;
|
|
82204
|
+
for (let i = start; i < entries.length && page.length < req.limit; i++) {
|
|
82205
|
+
const e = entries[i];
|
|
82206
|
+
const enc = encodedBytes(e) + 1;
|
|
82207
|
+
if (page.length > 0 && acc + enc > REPLY_BUDGET) break;
|
|
82208
|
+
page.push(e);
|
|
82209
|
+
acc += enc;
|
|
82210
|
+
}
|
|
82211
|
+
const consumed = start + page.length;
|
|
82212
|
+
return {
|
|
82213
|
+
agentId: req.agentId,
|
|
82214
|
+
path: req.path,
|
|
82215
|
+
exists: true,
|
|
82216
|
+
entries: page,
|
|
82217
|
+
...consumed < entries.length ? { nextCursor: String(consumed) } : {}
|
|
82218
|
+
};
|
|
82219
|
+
},
|
|
82220
|
+
async read(req) {
|
|
82221
|
+
const { resolved, realRoot } = await resolveContained(rootFor(req.agentId), req.path);
|
|
82222
|
+
const notFound = {
|
|
82223
|
+
agentId: req.agentId,
|
|
82224
|
+
path: req.path,
|
|
82225
|
+
exists: false
|
|
82226
|
+
};
|
|
82227
|
+
if (realRoot === null) return notFound;
|
|
82228
|
+
let st;
|
|
82229
|
+
try {
|
|
82230
|
+
st = await promises.lstat(resolved);
|
|
82231
|
+
} catch (err) {
|
|
82232
|
+
if (isErrno(err, "ENOENT")) return notFound;
|
|
82233
|
+
throw err;
|
|
82234
|
+
}
|
|
82235
|
+
if (!st.isFile()) throw new WorkspaceViolationError("not a regular file");
|
|
82236
|
+
const target = await canonicalUnder(realRoot, resolved);
|
|
82237
|
+
const size = st.size;
|
|
82238
|
+
const mtime = st.mtime.toISOString();
|
|
82239
|
+
const fh = await promises.open(target, "r");
|
|
82240
|
+
try {
|
|
82241
|
+
const sniffLen = Math.min(SNIFF_BYTES, size);
|
|
82242
|
+
if (sniffLen > 0) {
|
|
82243
|
+
const sniff = Buffer.alloc(sniffLen);
|
|
82244
|
+
const { bytesRead } = await fh.read(sniff, 0, sniffLen, 0);
|
|
82245
|
+
if (sniff.subarray(0, bytesRead).includes(0)) return {
|
|
82246
|
+
agentId: req.agentId,
|
|
82247
|
+
path: req.path,
|
|
82248
|
+
exists: true,
|
|
82249
|
+
size,
|
|
82250
|
+
mtime,
|
|
82251
|
+
encoding: "none"
|
|
82252
|
+
};
|
|
82253
|
+
}
|
|
82254
|
+
const want = Math.min(req.limit, Math.max(0, size - req.offset));
|
|
82255
|
+
let slice = Buffer.alloc(0);
|
|
82256
|
+
if (want > 0) {
|
|
82257
|
+
const buf = Buffer.alloc(want);
|
|
82258
|
+
const { bytesRead } = await fh.read(buf, 0, want, req.offset);
|
|
82259
|
+
slice = buf.subarray(0, bytesRead);
|
|
82260
|
+
}
|
|
82261
|
+
let end = utf8Boundary(slice, slice.length);
|
|
82262
|
+
let content = slice.toString("utf8", 0, end);
|
|
82263
|
+
while (end > 0 && encodedBytes(content) > REPLY_BUDGET) {
|
|
82264
|
+
const factor = REPLY_BUDGET / encodedBytes(content);
|
|
82265
|
+
const shrunk = Math.max(1, Math.floor(end * factor * .9));
|
|
82266
|
+
end = utf8Boundary(slice, Math.min(shrunk, end - 1));
|
|
82267
|
+
content = slice.toString("utf8", 0, end);
|
|
82268
|
+
}
|
|
82269
|
+
const nextOffset = req.offset + end;
|
|
82270
|
+
return {
|
|
82271
|
+
agentId: req.agentId,
|
|
82272
|
+
path: req.path,
|
|
82273
|
+
exists: true,
|
|
82274
|
+
size,
|
|
82275
|
+
mtime,
|
|
82276
|
+
encoding: "utf8",
|
|
82277
|
+
content,
|
|
82278
|
+
offset: req.offset,
|
|
82279
|
+
nextOffset,
|
|
82280
|
+
truncated: nextOffset < size
|
|
82281
|
+
};
|
|
82282
|
+
} finally {
|
|
82283
|
+
await fh.close();
|
|
82284
|
+
}
|
|
82285
|
+
}
|
|
82286
|
+
};
|
|
82287
|
+
}
|
|
82288
|
+
/** dirs first, then case-insensitive alphabetical (stable within the page). */
|
|
82289
|
+
function compareEntries(a, b) {
|
|
82290
|
+
const aDir = a.type === "dir" ? 0 : 1;
|
|
82291
|
+
const bDir = b.type === "dir" ? 0 : 1;
|
|
82292
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
82293
|
+
const al = a.name.toLowerCase();
|
|
82294
|
+
const bl = b.name.toLowerCase();
|
|
82295
|
+
if (al !== bl) return al < bl ? -1 : 1;
|
|
82296
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
82297
|
+
}
|
|
82298
|
+
//#endregion
|
|
81963
82299
|
//#region src/cp/correlator.ts
|
|
81964
82300
|
/** Transport-free representation of a typed wire `error` REP. */
|
|
81965
82301
|
var WireError = class extends Error {
|
|
@@ -82136,17 +82472,19 @@ var CpClient = class {
|
|
|
82136
82472
|
this.heartbeatMs = ok.heartbeatSec > 0 ? ok.heartbeatSec * 1e3 : this.deps.heartbeatDefaultMs;
|
|
82137
82473
|
this.armHeartbeat();
|
|
82138
82474
|
this.deps.log.info(`cp: READY (epoch=${this.sessionEpoch}, routingEpoch=${this.routingEpoch})`);
|
|
82139
|
-
|
|
82475
|
+
this.emitDaemonRuntimes(this.deps.runtimeProfiles());
|
|
82140
82476
|
this.deps.onReady?.();
|
|
82141
82477
|
}
|
|
82142
82478
|
/**
|
|
82143
|
-
* Push
|
|
82144
|
-
*
|
|
82145
|
-
*
|
|
82479
|
+
* Push the full runtime snapshot (D→C `facts/daemon-runtimes` EVT,
|
|
82480
|
+
* fire-and-forget) — sent on each register and again when a probe sweep
|
|
82481
|
+
* completes. REPLACE semantics on the CP: it reconciles the daemon's stored
|
|
82482
|
+
* runtime list to exactly this snapshot, pruning runtimes that are no longer
|
|
82483
|
+
* installed. No-op unless READY/DRAINING.
|
|
82146
82484
|
*/
|
|
82147
|
-
|
|
82485
|
+
emitDaemonRuntimes(runtimes) {
|
|
82148
82486
|
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
82149
|
-
this.transport?.send(encode(buildEnvelope("facts/
|
|
82487
|
+
this.transport?.send(encode(buildEnvelope("facts/daemon-runtimes", { runtimes })));
|
|
82150
82488
|
}
|
|
82151
82489
|
/**
|
|
82152
82490
|
* Report a session's cumulative token usage (D→C `usage/report` EVT,
|
|
@@ -82323,6 +82661,12 @@ var CpClient = class {
|
|
|
82323
82661
|
this.sendError(frame.id, "INTERNAL", `session/history failed: ${err.message}`, false);
|
|
82324
82662
|
}
|
|
82325
82663
|
return;
|
|
82664
|
+
case "workspace/list":
|
|
82665
|
+
this.deps.workspaceRead.list(frame.payload).then((page) => this.reply(frame, "workspace/list/page", page)).catch((err) => this.workspaceError(frame.id, "workspace/list", err));
|
|
82666
|
+
return;
|
|
82667
|
+
case "workspace/read":
|
|
82668
|
+
this.deps.workspaceRead.read(frame.payload).then((content) => this.reply(frame, "workspace/read/content", content)).catch((err) => this.workspaceError(frame.id, "workspace/read", err));
|
|
82669
|
+
return;
|
|
82326
82670
|
case "agent/prompt":
|
|
82327
82671
|
this.sendError(frame.id, "INTERNAL", "agent/prompt not implemented", false);
|
|
82328
82672
|
return;
|
|
@@ -82331,6 +82675,18 @@ var CpClient = class {
|
|
|
82331
82675
|
return;
|
|
82332
82676
|
}
|
|
82333
82677
|
}
|
|
82678
|
+
/** Map a workspace read failure onto the wire: containment/bad-request
|
|
82679
|
+
* violations → BAD_PAYLOAD (their messages are hand-written and path-free);
|
|
82680
|
+
* anything else → INTERNAL with a GENERIC message — raw fs errors (ELOOP,
|
|
82681
|
+
* EACCES, …) embed absolute host paths that must not leak to the CP/UI. */
|
|
82682
|
+
workspaceError(corr, op, err) {
|
|
82683
|
+
if (err instanceof WorkspaceViolationError) {
|
|
82684
|
+
this.sendError(corr, "BAD_PAYLOAD", `${op} failed: ${err.message}`, false);
|
|
82685
|
+
return;
|
|
82686
|
+
}
|
|
82687
|
+
this.deps.log.warn(`cp: ${op} failed: ${err?.message}`);
|
|
82688
|
+
this.sendError(corr, "INTERNAL", `${op} failed`, false);
|
|
82689
|
+
}
|
|
82334
82690
|
reply(req, type, payload) {
|
|
82335
82691
|
this.transport?.send(encode(buildEnvelope(type, payload, { corr: req.id })));
|
|
82336
82692
|
}
|
|
@@ -83893,6 +84249,7 @@ var Daemon = class Daemon {
|
|
|
83893
84249
|
degradedScopes: () => this.cpDegradedScopes(),
|
|
83894
84250
|
configApply: this.cpConfigApply(),
|
|
83895
84251
|
sessionRead: createSessionReader(this.store),
|
|
84252
|
+
workspaceRead: createWorkspaceReader((id) => this.agents.get(id)?.workspace.path),
|
|
83896
84253
|
clock: systemClock,
|
|
83897
84254
|
connect: () => ClientTransport.dial(url),
|
|
83898
84255
|
log: this.log
|
|
@@ -83900,8 +84257,9 @@ var Daemon = class Daemon {
|
|
|
83900
84257
|
this.cpClient.start();
|
|
83901
84258
|
this.log.info(`cp: connecting to ${url}…`);
|
|
83902
84259
|
}
|
|
83903
|
-
/** Build the current
|
|
83904
|
-
* models learned by the
|
|
84260
|
+
/** Build the current profile entry for a runtime (one element of the
|
|
84261
|
+
* `facts/daemon-runtimes` snapshot), folding in any models learned by the
|
|
84262
|
+
* probe sweep. */
|
|
83905
84263
|
runtimeProfileFor(id) {
|
|
83906
84264
|
return {
|
|
83907
84265
|
runtime: id,
|
|
@@ -83916,8 +84274,8 @@ var Daemon = class Daemon {
|
|
|
83916
84274
|
static PROBE_TTL_MS = 5 * 6e4;
|
|
83917
84275
|
/**
|
|
83918
84276
|
* Probe every installed runtime in the background (launch → initialize →
|
|
83919
|
-
* session/new → read models → tear down), then emit
|
|
83920
|
-
*
|
|
84277
|
+
* session/new → read models → tear down), then emit one `facts/daemon-runtimes`
|
|
84278
|
+
* snapshot once the whole sweep completes so the CP replaces its runtime list.
|
|
83921
84279
|
*
|
|
83922
84280
|
* Triggered on each CP (re)connect. Deduped while in flight; skipped (with a
|
|
83923
84281
|
* cached re-emit) when the last sweep is still fresh, so a reconnect storm can't
|
|
@@ -83930,7 +84288,7 @@ var Daemon = class Daemon {
|
|
|
83930
84288
|
if (ids.length === 0) return;
|
|
83931
84289
|
if (this.probing) return;
|
|
83932
84290
|
if (this.lastProbeAtMs > 0 && Date.now() - this.lastProbeAtMs < Daemon.PROBE_TTL_MS) {
|
|
83933
|
-
|
|
84291
|
+
this.cpClient?.emitDaemonRuntimes(ids.map((id) => this.runtimeProfileFor(id)));
|
|
83934
84292
|
return;
|
|
83935
84293
|
}
|
|
83936
84294
|
this.probing = true;
|
|
@@ -83941,7 +84299,7 @@ var Daemon = class Daemon {
|
|
|
83941
84299
|
this.lastProbeAtMs = Date.now();
|
|
83942
84300
|
const okCount = results.filter((r) => r.ok).length;
|
|
83943
84301
|
this.log.info(`probe: sweep complete — ${okCount}/${results.length} runtime(s) reachable`);
|
|
83944
|
-
|
|
84302
|
+
this.cpClient?.emitDaemonRuntimes(ids.map((id) => this.runtimeProfileFor(id)));
|
|
83945
84303
|
} catch (err) {
|
|
83946
84304
|
this.log.warn(`probe: sweep failed: ${formatErr(err)}`);
|
|
83947
84305
|
} finally {
|