@byok-sdk/client 0.6.1 → 0.8.0-beta.0
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 +11 -8
- package/dist/bin/byok-agent.js +142 -47
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/daemon/agent-egress-controller.d.ts +3 -0
- package/dist/daemon/create-daemon.d.ts +4 -10
- package/dist/daemon/store.d.ts +9 -0
- package/dist/daemon/task-runner.d.ts +2 -2
- package/dist/index.js +141 -46
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -49,8 +49,11 @@ export declare class AgentEgressController {
|
|
|
49
49
|
private readonly latestStatus;
|
|
50
50
|
private readonly reliableStatus;
|
|
51
51
|
private readonly drops;
|
|
52
|
+
private active;
|
|
52
53
|
constructor(options: AgentEgressControllerOptions);
|
|
53
54
|
get policy(): Readonly<AgentEgressPolicy>;
|
|
55
|
+
/** Permanently fail closed after its authenticated enrollment is replaced. */
|
|
56
|
+
deactivate(): void;
|
|
54
57
|
status(): AgentEgressStatus;
|
|
55
58
|
dropReceipts(): readonly AgentEgressDropReceipt[];
|
|
56
59
|
noteTransportDrop(reason: AgentEgressDropReason, agentRef?: AgentRef): void;
|
|
@@ -62,13 +62,6 @@ export interface HostedJournalConfig {
|
|
|
62
62
|
* to a closed set instead of a re-interpretation of an existing config.
|
|
63
63
|
*/
|
|
64
64
|
mode: 'sqlite';
|
|
65
|
-
/**
|
|
66
|
-
* The tenant every journal row on this device is scoped to (§12.7.2's
|
|
67
|
-
* minimum fact set opens with tenant/product/device). Required: a hosted
|
|
68
|
-
* daemon that cannot say which tenant its durable evidence belongs to has
|
|
69
|
-
* evidence nobody can act on.
|
|
70
|
-
*/
|
|
71
|
-
tenantId: string;
|
|
72
65
|
/** Bound on waiting for the journal's write lock, ms. Defaults to the journal's own bound. */
|
|
73
66
|
busyTimeoutMs?: number;
|
|
74
67
|
/** Per-record byte bound. Defaults to the journal's own bound; oversized records are refused, never truncated. */
|
|
@@ -347,8 +340,6 @@ export interface DaemonConfig {
|
|
|
347
340
|
deviceAssertion?: DeviceAssertionConfig;
|
|
348
341
|
}
|
|
349
342
|
export interface AgentEgressConfig {
|
|
350
|
-
/** Authenticated deployment tenant bound into every local reliable record. */
|
|
351
|
-
tenantId: string;
|
|
352
343
|
/** Exact policy the daemon is willing to consume from an Agent offer. */
|
|
353
344
|
policy: AgentEgressPolicy;
|
|
354
345
|
/** Named redaction hook for explicit contentful trajectory only. */
|
|
@@ -381,8 +372,11 @@ export interface AgentContentReadConfig {
|
|
|
381
372
|
export interface AgentReliableEgressInput {
|
|
382
373
|
agentRef: AgentRef;
|
|
383
374
|
sessionRef: string;
|
|
375
|
+
/** Exact runtime identity from the durable Agent-home handoff. */
|
|
376
|
+
runtimeId: string;
|
|
377
|
+
/** Exact task identity from the same durable Agent-home handoff. */
|
|
378
|
+
taskId: string;
|
|
384
379
|
payload: unknown;
|
|
385
|
-
taskId?: string;
|
|
386
380
|
eventId?: string;
|
|
387
381
|
}
|
|
388
382
|
/**
|
package/dist/daemon/store.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type EnsureSecureDirOptions } from '../util/secure-dir';
|
|
2
2
|
export interface DeviceRecord {
|
|
3
3
|
deviceId: string;
|
|
4
|
+
/** Opaque tenant binding returned by the authenticated pairing response. */
|
|
5
|
+
tenantId: string;
|
|
4
6
|
/** Current access token (JWT), renewed via challenge/token without re-pairing (protocol §6.2). */
|
|
5
7
|
accessToken: string;
|
|
6
8
|
/** ISO-8601 expiry for `accessToken` (our best knowledge of it — see auth-manager.ts for how this is derived after `/byok/pair`, which reports no explicit expiry itself). */
|
|
@@ -10,6 +12,13 @@ export interface DeviceRecord {
|
|
|
10
12
|
/** Ed25519 public key, base64url — re-sent verbatim on a post-revocation re-pair (protocol §6.3). */
|
|
11
13
|
devicePublicKey: string;
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* A durable enrollment record cannot be used by any steady-state path. Only
|
|
17
|
+
* the explicit pair operation may replace it with a fresh authenticated row.
|
|
18
|
+
*/
|
|
19
|
+
export declare class DeviceRecordRePairRequiredError extends Error {
|
|
20
|
+
constructor();
|
|
21
|
+
}
|
|
13
22
|
/**
|
|
14
23
|
* Persists the device identity issued by `pair()` — deviceId, current
|
|
15
24
|
* access token + its expiry, and the device's own Ed25519 keypair. This is
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentEgressPolicy, type Envelope, type PermissionPolicy, type RuntimeId, type TaskOfferPayload, type TaskOfferForAgentPayload, type TaskOfferForAgentWithEgressPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
|
|
1
|
+
import { type AgentEgressPolicy, type Envelope, type PermissionPolicy, type RuntimeId, type TaskOfferPayload, type TaskOfferForAgentPayload, type TaskOfferForAgentWithEgressPayload, type TaskOfferForAgentWithEgressFreshPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
|
|
2
2
|
import { type McpToolsetConfig, type RuntimeAdapter } from '../types';
|
|
3
3
|
import { AgentHomeManager, type AgentRef } from '../agent-home';
|
|
4
4
|
import { AgentSessionHandoffStore, type AgentTerminalCause } from './agent-session-handoff-store';
|
|
@@ -366,7 +366,7 @@ export type AdmissionGuardDecision = {
|
|
|
366
366
|
readonly reason: string;
|
|
367
367
|
readonly retryable: boolean;
|
|
368
368
|
};
|
|
369
|
-
type AcceptedOfferPayload = TaskOfferPayload | TaskOfferWithToolsetsPayload | TaskOfferForAgentPayload | TaskOfferForAgentWithEgressPayload;
|
|
369
|
+
type AcceptedOfferPayload = TaskOfferPayload | TaskOfferWithToolsetsPayload | TaskOfferForAgentPayload | TaskOfferForAgentWithEgressPayload | TaskOfferForAgentWithEgressFreshPayload;
|
|
370
370
|
/**
|
|
371
371
|
* Per-connection task orchestration: offer -> (decline | prepare -> seal ->
|
|
372
372
|
* claim -> prepared operation -> started) -> seq-ordered progress batches -> complete/fail/
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID, createHash, sign, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
|
|
2
2
|
import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, readFileSync, realpathSync } from 'fs';
|
|
3
3
|
import path, { join, isAbsolute } from 'path';
|
|
4
|
-
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, decodeEnvelope, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
4
|
+
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, decodeEnvelope, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
5
5
|
import { execFile, spawn } from 'child_process';
|
|
6
6
|
import os6 from 'os';
|
|
7
|
-
import { parseDeviceAssertionEnvelope, tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash as contentHash$1, TRUTH_RECORD_KINDS, nonceSigningBytes, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
7
|
+
import { parseDeviceAssertionEnvelope, tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash as contentHash$1, TRUTH_RECORD_KINDS, nonceSigningBytes, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, isTenantId, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
8
8
|
import { promisify } from 'util';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import 'readline';
|
|
@@ -4683,6 +4683,13 @@ var ApprovalRegistry = class {
|
|
|
4683
4683
|
}
|
|
4684
4684
|
};
|
|
4685
4685
|
var MAX_DEVICE_RECORD_BYTES = 256 * 1024;
|
|
4686
|
+
var REPAIR_REQUIRED_MESSAGE = "device enrollment record is missing or has an invalid authenticated tenant binding; re-pair required";
|
|
4687
|
+
var DeviceRecordRePairRequiredError = class extends Error {
|
|
4688
|
+
constructor() {
|
|
4689
|
+
super(REPAIR_REQUIRED_MESSAGE);
|
|
4690
|
+
this.name = "DeviceRecordRePairRequiredError";
|
|
4691
|
+
}
|
|
4692
|
+
};
|
|
4686
4693
|
function sameInode(left, right) {
|
|
4687
4694
|
return left.dev === right.dev && left.ino === right.ino;
|
|
4688
4695
|
}
|
|
@@ -4692,18 +4699,32 @@ function sameFileState(left, right) {
|
|
|
4692
4699
|
function sameContentState(left, right) {
|
|
4693
4700
|
return sameInode(left, right) && left.size === right.size && left.mtimeNs === right.mtimeNs;
|
|
4694
4701
|
}
|
|
4702
|
+
function assertDeviceRecord(value) {
|
|
4703
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4704
|
+
throw new DeviceRecordRePairRequiredError();
|
|
4705
|
+
}
|
|
4706
|
+
const parsed = value;
|
|
4707
|
+
if (typeof parsed.deviceId === "string" && isTenantId(parsed.tenantId) && typeof parsed.accessToken === "string" && typeof parsed.expiresAt === "string" && typeof parsed.devicePrivateKeyPem === "string" && typeof parsed.devicePublicKey === "string") {
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
throw new DeviceRecordRePairRequiredError();
|
|
4711
|
+
}
|
|
4695
4712
|
function parseDeviceRecord(raw) {
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
expiresAt: parsed.expiresAt,
|
|
4702
|
-
devicePrivateKeyPem: parsed.devicePrivateKeyPem,
|
|
4703
|
-
devicePublicKey: parsed.devicePublicKey
|
|
4704
|
-
};
|
|
4713
|
+
let parsed;
|
|
4714
|
+
try {
|
|
4715
|
+
parsed = JSON.parse(raw);
|
|
4716
|
+
} catch {
|
|
4717
|
+
throw new DeviceRecordRePairRequiredError();
|
|
4705
4718
|
}
|
|
4706
|
-
|
|
4719
|
+
assertDeviceRecord(parsed);
|
|
4720
|
+
return {
|
|
4721
|
+
deviceId: parsed.deviceId,
|
|
4722
|
+
tenantId: parsed.tenantId,
|
|
4723
|
+
accessToken: parsed.accessToken,
|
|
4724
|
+
expiresAt: parsed.expiresAt,
|
|
4725
|
+
devicePrivateKeyPem: parsed.devicePrivateKeyPem,
|
|
4726
|
+
devicePublicKey: parsed.devicePublicKey
|
|
4727
|
+
};
|
|
4707
4728
|
}
|
|
4708
4729
|
var DeviceStore = class _DeviceStore {
|
|
4709
4730
|
/**
|
|
@@ -4773,6 +4794,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
4773
4794
|
}
|
|
4774
4795
|
}
|
|
4775
4796
|
async save(record) {
|
|
4797
|
+
assertDeviceRecord(record);
|
|
4776
4798
|
const storeDir = path.dirname(this.filePath);
|
|
4777
4799
|
await ensureSecureDir(storeDir, this.secureDirOptions);
|
|
4778
4800
|
await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
|
|
@@ -4943,7 +4965,14 @@ var AuthManager = class {
|
|
|
4943
4965
|
this.proactiveTimer = void 0;
|
|
4944
4966
|
try {
|
|
4945
4967
|
return await this.runCredentialMutation(async () => {
|
|
4946
|
-
|
|
4968
|
+
let existing = this.record;
|
|
4969
|
+
if (!existing) {
|
|
4970
|
+
try {
|
|
4971
|
+
existing = await this.opts.store.load();
|
|
4972
|
+
} catch (error) {
|
|
4973
|
+
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4947
4976
|
const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
|
|
4948
4977
|
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
4949
4978
|
const res = await fetch(url, {
|
|
@@ -4958,9 +4987,10 @@ var AuthManager = class {
|
|
|
4958
4987
|
if (!res.ok) {
|
|
4959
4988
|
throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
|
|
4960
4989
|
}
|
|
4961
|
-
const body = await res.json();
|
|
4990
|
+
const body = PairResponseSchema.parse(await res.json());
|
|
4962
4991
|
const record = {
|
|
4963
4992
|
deviceId: body.deviceId,
|
|
4993
|
+
tenantId: body.tenantId,
|
|
4964
4994
|
accessToken: body.accessToken,
|
|
4965
4995
|
expiresAt: resolvePairExpiry(body.refreshHint),
|
|
4966
4996
|
devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey),
|
|
@@ -9797,6 +9827,11 @@ function offeredAgentRef(payload) {
|
|
|
9797
9827
|
if (!Object.prototype.hasOwnProperty.call(payload, "agentRef")) return void 0;
|
|
9798
9828
|
return validateAgentRef(payload.agentRef);
|
|
9799
9829
|
}
|
|
9830
|
+
function offeredSessionRef(payload) {
|
|
9831
|
+
if (!Object.prototype.hasOwnProperty.call(payload, "sessionRef")) return void 0;
|
|
9832
|
+
const value = payload.sessionRef;
|
|
9833
|
+
return typeof value === "string" ? value : void 0;
|
|
9834
|
+
}
|
|
9800
9835
|
function errorMessage4(err) {
|
|
9801
9836
|
return err instanceof Error ? err.message : String(err);
|
|
9802
9837
|
}
|
|
@@ -10165,6 +10200,9 @@ var TaskRunner = class {
|
|
|
10165
10200
|
case "task.offer_for_agent_with_egress":
|
|
10166
10201
|
await this.handleOffer(envelope.task_id, envelope.payload, true);
|
|
10167
10202
|
return;
|
|
10203
|
+
case "task.offer_for_agent_with_egress_fresh":
|
|
10204
|
+
await this.handleOffer(envelope.task_id, envelope.payload, true);
|
|
10205
|
+
return;
|
|
10168
10206
|
case "task.cancel":
|
|
10169
10207
|
await this.handleCancel(envelope.task_id, envelope.payload.reason);
|
|
10170
10208
|
return;
|
|
@@ -10206,6 +10244,7 @@ var TaskRunner = class {
|
|
|
10206
10244
|
const decline = (reason, retryable) => {
|
|
10207
10245
|
this.decline(taskId, reason, retryable, agentRef);
|
|
10208
10246
|
};
|
|
10247
|
+
const sessionRef = offeredSessionRef(payload);
|
|
10209
10248
|
if ("egressPolicy" in payload) {
|
|
10210
10249
|
if (this.deps.agentEgressPolicy === void 0 || !sameEgressPolicy(this.deps.agentEgressPolicy, payload.egressPolicy)) {
|
|
10211
10250
|
decline("Agent egress offer policy is not exactly enabled by this daemon", false);
|
|
@@ -10307,11 +10346,11 @@ var TaskRunner = class {
|
|
|
10307
10346
|
let plainWorkspaceNeedsResolve = false;
|
|
10308
10347
|
if (agentBinding !== void 0) {
|
|
10309
10348
|
workspaceDir = agentBinding.lease.cwd;
|
|
10310
|
-
if (
|
|
10349
|
+
if (sessionRef !== void 0) {
|
|
10311
10350
|
try {
|
|
10312
10351
|
await this.deps.agentSessionHandoffs.requireMatch({
|
|
10313
10352
|
agentRef: agentBinding.resolution.agentRef,
|
|
10314
|
-
sessionRef
|
|
10353
|
+
sessionRef,
|
|
10315
10354
|
runtimeId: pick.descriptor.id,
|
|
10316
10355
|
cwd: workspaceDir
|
|
10317
10356
|
});
|
|
@@ -10333,15 +10372,15 @@ var TaskRunner = class {
|
|
|
10333
10372
|
return;
|
|
10334
10373
|
}
|
|
10335
10374
|
} else if (this.deps.gitWorkspaceManager && this.deps.gitWorkspaceStore) {
|
|
10336
|
-
known =
|
|
10375
|
+
known = sessionRef ? await this.deps.sessionWorkspaces.get(sessionRef) : void 0;
|
|
10337
10376
|
const gitManager = this.deps.gitWorkspaceManager;
|
|
10338
10377
|
const gitStore = this.deps.gitWorkspaceStore;
|
|
10339
|
-
if (
|
|
10340
|
-
const ledger = await gitStore.findBySessionAnyPhase(
|
|
10378
|
+
if (sessionRef) {
|
|
10379
|
+
const ledger = await gitStore.findBySessionAnyPhase(sessionRef).catch(() => void 0);
|
|
10341
10380
|
const sameProtocolTask = ledger?.taskId === taskId;
|
|
10342
10381
|
const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
|
|
10343
10382
|
const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
|
|
10344
|
-
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !==
|
|
10383
|
+
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== sessionRef || path.resolve(ledger.workspaceDir) !== path.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
|
|
10345
10384
|
decline("session is incompatible with Git workspace mode", true);
|
|
10346
10385
|
return;
|
|
10347
10386
|
}
|
|
@@ -10360,13 +10399,13 @@ var TaskRunner = class {
|
|
|
10360
10399
|
gitWorkspaceId = randomUUID();
|
|
10361
10400
|
}
|
|
10362
10401
|
try {
|
|
10363
|
-
gitLease = await gitManager.acquireLease(workspaceDir,
|
|
10402
|
+
gitLease = await gitManager.acquireLease(workspaceDir, sessionRef);
|
|
10364
10403
|
} catch {
|
|
10365
10404
|
decline("workspace is busy or unavailable", true);
|
|
10366
10405
|
return;
|
|
10367
10406
|
}
|
|
10368
10407
|
} else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
|
|
10369
|
-
known =
|
|
10408
|
+
known = sessionRef ? await this.deps.sessionWorkspaces.get(sessionRef) : void 0;
|
|
10370
10409
|
workspaceDir = known?.workspaceDir ?? path.join(this.deps.workspaceRoot, taskId);
|
|
10371
10410
|
plainWorkspaceNeedsResolve = true;
|
|
10372
10411
|
} else {
|
|
@@ -10385,7 +10424,7 @@ var TaskRunner = class {
|
|
|
10385
10424
|
policy: decision.policy,
|
|
10386
10425
|
requiredToolsetIds: requiredToolsets ?? [],
|
|
10387
10426
|
...offered.dispatchSelection === void 0 ? {} : { dispatchSelection: offered.dispatchSelection },
|
|
10388
|
-
...
|
|
10427
|
+
...sessionRef === void 0 || known === void 0 && agentBinding === void 0 ? {} : { sessionRef },
|
|
10389
10428
|
...agentBinding === void 0 ? {} : {
|
|
10390
10429
|
agentRef: agentBinding.resolution.agentRef,
|
|
10391
10430
|
cwd: agentBinding.lease.cwd,
|
|
@@ -10488,7 +10527,7 @@ var TaskRunner = class {
|
|
|
10488
10527
|
workspaceId,
|
|
10489
10528
|
taskId,
|
|
10490
10529
|
workspaceDir,
|
|
10491
|
-
sessionRef
|
|
10530
|
+
sessionRef,
|
|
10492
10531
|
phase,
|
|
10493
10532
|
baseline: gitBaseline ?? observation.head,
|
|
10494
10533
|
current: observation.head,
|
|
@@ -12345,9 +12384,14 @@ var AgentEgressController = class {
|
|
|
12345
12384
|
latestStatus = emptyLane();
|
|
12346
12385
|
reliableStatus = emptyLane();
|
|
12347
12386
|
drops = [];
|
|
12387
|
+
active = true;
|
|
12348
12388
|
get policy() {
|
|
12349
12389
|
return this.options.policy;
|
|
12350
12390
|
}
|
|
12391
|
+
/** Permanently fail closed after its authenticated enrollment is replaced. */
|
|
12392
|
+
deactivate() {
|
|
12393
|
+
this.active = false;
|
|
12394
|
+
}
|
|
12351
12395
|
status() {
|
|
12352
12396
|
const reliable = this.reliableRecords();
|
|
12353
12397
|
return Object.freeze({
|
|
@@ -12365,6 +12409,10 @@ var AgentEgressController = class {
|
|
|
12365
12409
|
/** Project before TaskRunner builds a `task.progress` envelope. */
|
|
12366
12410
|
projectLatestValue(input) {
|
|
12367
12411
|
if (input.agentRef === void 0) return Object.freeze([...input.events]);
|
|
12412
|
+
if (!this.active) {
|
|
12413
|
+
this.noteDrop("latest-value", "policy_denied", input.agentRef);
|
|
12414
|
+
return [];
|
|
12415
|
+
}
|
|
12368
12416
|
if (this.options.policy.activity.mode === "contentful-trajectory" && !input.serverCapabilities.includes("agent-egress-policy")) {
|
|
12369
12417
|
this.noteDrop("latest-value", "capability_missing", input.agentRef);
|
|
12370
12418
|
return [];
|
|
@@ -12387,7 +12435,7 @@ var AgentEgressController = class {
|
|
|
12387
12435
|
return latest === void 0 ? [] : Object.freeze([latest]);
|
|
12388
12436
|
}
|
|
12389
12437
|
async appendReliable(input) {
|
|
12390
|
-
if (this.options.tenantId === void 0) {
|
|
12438
|
+
if (!this.active || this.options.tenantId === void 0) {
|
|
12391
12439
|
this.noteDrop("reliable", "policy_denied", input.agentRef);
|
|
12392
12440
|
return { ok: false, reason: "policy_denied" };
|
|
12393
12441
|
}
|
|
@@ -12425,7 +12473,7 @@ var AgentEgressController = class {
|
|
|
12425
12473
|
* with `wireType: agent.content.receipt` before any transport attempt.
|
|
12426
12474
|
*/
|
|
12427
12475
|
async appendContentReceipt(input) {
|
|
12428
|
-
if (this.options.tenantId === void 0) {
|
|
12476
|
+
if (!this.active || this.options.tenantId === void 0) {
|
|
12429
12477
|
this.noteDrop("reliable", "policy_denied", input.agentRef);
|
|
12430
12478
|
return { ok: false, reason: "policy_denied" };
|
|
12431
12479
|
}
|
|
@@ -12449,7 +12497,7 @@ var AgentEgressController = class {
|
|
|
12449
12497
|
/** Retires only the record whose full Agent/tenant/revision/id/cursor tuple matches. */
|
|
12450
12498
|
async acknowledge(ack) {
|
|
12451
12499
|
const spool = this.spools.get(agentKey(ack.agentRef));
|
|
12452
|
-
if (!spool || this.options.tenantId === void 0 || ack.tenantId !== this.options.tenantId) {
|
|
12500
|
+
if (!this.active || !spool || this.options.tenantId === void 0 || ack.tenantId !== this.options.tenantId) {
|
|
12453
12501
|
this.noteDrop("reliable", "ack_mismatch", ack.agentRef);
|
|
12454
12502
|
return false;
|
|
12455
12503
|
}
|
|
@@ -12460,6 +12508,7 @@ var AgentEgressController = class {
|
|
|
12460
12508
|
/** Re-open every existing Agent-local spool before retrying stable records after restart. */
|
|
12461
12509
|
async recover(agentsRoot) {
|
|
12462
12510
|
if (!path.isAbsolute(agentsRoot)) throw new Error("Agent egress recovery root must be absolute");
|
|
12511
|
+
if (!this.active) throw new Error("Agent egress recovery requires an active authenticated enrollment");
|
|
12463
12512
|
if (this.options.tenantId === void 0) {
|
|
12464
12513
|
throw new Error("Agent egress recovery requires one authenticated tenant authority");
|
|
12465
12514
|
}
|
|
@@ -13445,7 +13494,13 @@ function computeCapabilities(adapters, agentHomeConfigured = false, agentEgressC
|
|
|
13445
13494
|
flags.push("toolset-selection");
|
|
13446
13495
|
}
|
|
13447
13496
|
if (agentHomeConfigured) flags.push("agent-home-contract");
|
|
13448
|
-
if (agentEgressConfigured)
|
|
13497
|
+
if (agentEgressConfigured) {
|
|
13498
|
+
flags.push(
|
|
13499
|
+
AGENT_EGRESS_POLICY_CAPABILITY,
|
|
13500
|
+
AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
|
|
13501
|
+
AGENT_EGRESS_FRESH_SESSION_CAPABILITY
|
|
13502
|
+
);
|
|
13503
|
+
}
|
|
13449
13504
|
if (contentReadPolicies !== void 0) {
|
|
13450
13505
|
for (const surface of Object.keys(AGENT_CONTENT_READ_CAPABILITIES)) {
|
|
13451
13506
|
const policy = contentReadPolicies[surface];
|
|
@@ -13567,9 +13622,6 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13567
13622
|
if (config.agentEgress !== void 0 && config.agentHome === void 0) {
|
|
13568
13623
|
throw new Error("DaemonConfig.agentEgress requires DaemonConfig.agentHome for the per-Agent local spool");
|
|
13569
13624
|
}
|
|
13570
|
-
if (config.agentEgress !== void 0 && (config.agentEgress.tenantId.length === 0 || config.agentEgress.tenantId.trim() !== config.agentEgress.tenantId)) {
|
|
13571
|
-
throw new Error("DaemonConfig.agentEgress.tenantId must be a non-empty canonical authenticated tenant id");
|
|
13572
|
-
}
|
|
13573
13625
|
const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
|
|
13574
13626
|
const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
|
|
13575
13627
|
...config.progressBatch,
|
|
@@ -13600,9 +13652,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13600
13652
|
});
|
|
13601
13653
|
const agentSessionHandoffs = config.agentHome === void 0 ? void 0 : new AgentSessionHandoffStore();
|
|
13602
13654
|
const agentContentReadPolicies = resolveContentReadPolicies(egressPolicy, config.agentEgress?.contentRead);
|
|
13603
|
-
|
|
13655
|
+
let agentEgress = new AgentEgressController({
|
|
13604
13656
|
policy: egressPolicy,
|
|
13605
|
-
...config.agentEgress?.tenantId === void 0 ? {} : { tenantId: config.agentEgress.tenantId },
|
|
13606
13657
|
...config.agentEgress?.sanitizer === void 0 ? {} : { sanitizer: config.agentEgress.sanitizer }
|
|
13607
13658
|
});
|
|
13608
13659
|
const gitWorkspaceManager = config.gitWorkspace ? overrides.gitWorkspace?.manager ?? new GitWorkspaceManager(config.workspaceRoot, { ownerId: stableGitWorkspaceOwnerId(storeDir, config.productId) }) : void 0;
|
|
@@ -13612,11 +13663,6 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13612
13663
|
if (config.hostedJournal.mode !== "sqlite") {
|
|
13613
13664
|
throw new Error(`DaemonConfig.hostedJournal.mode must be "sqlite" \u2014 got ${JSON.stringify(config.hostedJournal.mode)}`);
|
|
13614
13665
|
}
|
|
13615
|
-
if (typeof config.hostedJournal.tenantId !== "string" || config.hostedJournal.tenantId.trim() === "") {
|
|
13616
|
-
throw new Error(
|
|
13617
|
-
"DaemonConfig.hostedJournal.tenantId must be a non-empty tenant id \u2014 a hosted journal row with no tenant is durable evidence nobody can act on"
|
|
13618
|
-
);
|
|
13619
|
-
}
|
|
13620
13666
|
if (config.hostedJournal.storagePolicy) {
|
|
13621
13667
|
resolvedStoragePolicy = resolveLocalStoragePolicy(config.hostedJournal.storagePolicy);
|
|
13622
13668
|
}
|
|
@@ -13685,6 +13731,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13685
13731
|
const approvalRegistry = new ApprovalRegistry();
|
|
13686
13732
|
let connection;
|
|
13687
13733
|
let connectionState = "closed";
|
|
13734
|
+
let daemonStarted = false;
|
|
13735
|
+
let tenantRebinding = false;
|
|
13688
13736
|
let runner;
|
|
13689
13737
|
let controlServerHandle;
|
|
13690
13738
|
let daemonOwnerLease;
|
|
@@ -13737,11 +13785,31 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13737
13785
|
return runLifecycleMutation(() => pairUnderLease(pairingCode));
|
|
13738
13786
|
}
|
|
13739
13787
|
async function pairUnderLease(pairingCode) {
|
|
13788
|
+
const wasRunning = daemonStarted;
|
|
13789
|
+
if (wasRunning) tenantRebinding = true;
|
|
13740
13790
|
const acquiredHere = daemonOwnerLease === void 0;
|
|
13741
13791
|
if (acquiredHere) daemonOwnerLease = await acquireDaemonOwner(storeDir, "daemon");
|
|
13792
|
+
let replacementPersisted = false;
|
|
13742
13793
|
try {
|
|
13743
|
-
|
|
13794
|
+
let previous;
|
|
13795
|
+
try {
|
|
13796
|
+
previous = await store.load();
|
|
13797
|
+
} catch (error) {
|
|
13798
|
+
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
13799
|
+
}
|
|
13744
13800
|
const record = await auth.pair(pairingCode);
|
|
13801
|
+
replacementPersisted = true;
|
|
13802
|
+
if (config.agentEgress !== void 0) {
|
|
13803
|
+
agentEgress.deactivate();
|
|
13804
|
+
agentEgress = new AgentEgressController({
|
|
13805
|
+
policy: egressPolicy,
|
|
13806
|
+
...config.agentEgress.sanitizer === void 0 ? {} : { sanitizer: config.agentEgress.sanitizer }
|
|
13807
|
+
});
|
|
13808
|
+
}
|
|
13809
|
+
if (wasRunning) {
|
|
13810
|
+
await runShutdownSequence("re-pairing enrollment binding");
|
|
13811
|
+
tenantRebinding = false;
|
|
13812
|
+
}
|
|
13745
13813
|
if (previous && previous.deviceId !== record.deviceId) {
|
|
13746
13814
|
await cursorStore.clear(config.serverUrl, previous.deviceId);
|
|
13747
13815
|
}
|
|
@@ -13752,6 +13820,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13752
13820
|
}
|
|
13753
13821
|
return record;
|
|
13754
13822
|
} catch (err) {
|
|
13823
|
+
if (wasRunning && !replacementPersisted) tenantRebinding = false;
|
|
13755
13824
|
if (acquiredHere) {
|
|
13756
13825
|
await daemonOwnerLease?.release();
|
|
13757
13826
|
daemonOwnerLease = void 0;
|
|
@@ -13774,6 +13843,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13774
13843
|
if (!record) {
|
|
13775
13844
|
throw new Error("device is not paired yet; call pair(pairingCode) first");
|
|
13776
13845
|
}
|
|
13846
|
+
if (config.agentEgress !== void 0) {
|
|
13847
|
+
agentEgress = new AgentEgressController({
|
|
13848
|
+
policy: egressPolicy,
|
|
13849
|
+
tenantId: record.tenantId,
|
|
13850
|
+
...config.agentEgress.sanitizer === void 0 ? {} : { sanitizer: config.agentEgress.sanitizer }
|
|
13851
|
+
});
|
|
13852
|
+
}
|
|
13777
13853
|
await agentHomeManager?.preflight();
|
|
13778
13854
|
if (config.agentEgress !== void 0 && config.agentHome !== void 0) {
|
|
13779
13855
|
await agentEgress.recover(path.join(config.agentHome.hostStorageRoot, "agents"));
|
|
@@ -13834,7 +13910,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13834
13910
|
config.agentEgress !== void 0,
|
|
13835
13911
|
agentContentReadPolicies
|
|
13836
13912
|
);
|
|
13837
|
-
const journalIdentity = config.hostedJournal ? { tenantId:
|
|
13913
|
+
const journalIdentity = config.hostedJournal ? { tenantId: record.tenantId, productId: config.productId, deviceId: record.deviceId } : void 0;
|
|
13838
13914
|
const sendSanitizedEnvelope = activeJournal && journalIdentity ? (envelope) => {
|
|
13839
13915
|
observer.handleOutboundEnvelope(envelope);
|
|
13840
13916
|
const terminalKind = terminalKindOf(envelope.type);
|
|
@@ -13972,9 +14048,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
13972
14048
|
runner = new TaskRunner(deps);
|
|
13973
14049
|
const handleAgentEgressEnvelope = async (envelope) => {
|
|
13974
14050
|
if (envelope.type !== "agent.egress.ack") return false;
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
await agentEgress.acknowledge({ ...envelope.payload, tenantId: tenantId2 });
|
|
14051
|
+
if (config.agentEgress === void 0) return true;
|
|
14052
|
+
await agentEgress.acknowledge({ ...envelope.payload, tenantId: record.tenantId });
|
|
13978
14053
|
return true;
|
|
13979
14054
|
};
|
|
13980
14055
|
const handleAgentContentReadEnvelope = async (envelope) => {
|
|
@@ -14026,7 +14101,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14026
14101
|
const result = await policyEngine.read({
|
|
14027
14102
|
requestId: payload.requestId,
|
|
14028
14103
|
actor: payload.actor,
|
|
14029
|
-
tenantId:
|
|
14104
|
+
tenantId: record.tenantId,
|
|
14030
14105
|
deviceId: record.deviceId,
|
|
14031
14106
|
agentRef: payload.agentRef,
|
|
14032
14107
|
surface: payload.surface,
|
|
@@ -14122,6 +14197,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14122
14197
|
//
|
|
14123
14198
|
// The no-journal branch is the ORIGINAL closure, unchanged.
|
|
14124
14199
|
onEnvelope: activeJournal && journalIdentity ? async (envelope) => {
|
|
14200
|
+
if (tenantRebinding) {
|
|
14201
|
+
throw new Error("tenant enrollment is being re-paired; inbound work is blocked until restart");
|
|
14202
|
+
}
|
|
14125
14203
|
observer.handleInboundEnvelope(envelope);
|
|
14126
14204
|
if (await handleAgentEgressEnvelope(envelope)) return;
|
|
14127
14205
|
if (await handleAgentContentReadEnvelope(envelope)) return;
|
|
@@ -14129,6 +14207,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14129
14207
|
await activeJournal.appendEnvelope(toJournalEnvelopeRecord(envelope, journalIdentity));
|
|
14130
14208
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
14131
14209
|
} : (envelope) => {
|
|
14210
|
+
if (tenantRebinding) {
|
|
14211
|
+
return Promise.reject(new Error("tenant enrollment is being re-paired; inbound work is blocked until restart"));
|
|
14212
|
+
}
|
|
14132
14213
|
observer.handleInboundEnvelope(envelope);
|
|
14133
14214
|
if (envelope.type === "agent.egress.ack") return handleAgentEgressEnvelope(envelope).then(() => void 0);
|
|
14134
14215
|
if (envelope.type === "agent.content.read") return handleAgentContentReadEnvelope(envelope).then(() => void 0);
|
|
@@ -14160,6 +14241,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14160
14241
|
dispatchReliableRecord(record2);
|
|
14161
14242
|
}
|
|
14162
14243
|
startPresenceProducer();
|
|
14244
|
+
daemonStarted = true;
|
|
14163
14245
|
} catch (err) {
|
|
14164
14246
|
try {
|
|
14165
14247
|
await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
|
|
@@ -14304,6 +14386,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14304
14386
|
if (!mutationBarrierComplete) {
|
|
14305
14387
|
throw new Error("daemon shutdown mutation barrier is incomplete; ownership lease retained");
|
|
14306
14388
|
}
|
|
14389
|
+
daemonStarted = false;
|
|
14307
14390
|
}
|
|
14308
14391
|
async function stop(opts = {}) {
|
|
14309
14392
|
shuttingDown = true;
|
|
@@ -14622,18 +14705,30 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
14622
14705
|
connection.send(sanitized.envelope);
|
|
14623
14706
|
}
|
|
14624
14707
|
async function publishReliableAgentEgress(input) {
|
|
14625
|
-
if (config.agentEgress === void 0 || agentHomeManager === void 0) {
|
|
14708
|
+
if (config.agentEgress === void 0 || agentHomeManager === void 0 || agentSessionHandoffs === void 0) {
|
|
14626
14709
|
throw new Error("Agent reliable egress is not configured");
|
|
14627
14710
|
}
|
|
14711
|
+
if (tenantRebinding) {
|
|
14712
|
+
throw new Error("tenant enrollment is being re-paired; reliable egress is blocked until restart");
|
|
14713
|
+
}
|
|
14628
14714
|
const binding = await agentHomeManager.acquire(input.agentRef);
|
|
14629
14715
|
try {
|
|
14630
14716
|
await agentHomeManager.initialize(binding);
|
|
14717
|
+
const handoff = await agentSessionHandoffs.requireMatch({
|
|
14718
|
+
agentRef: binding.resolution.agentRef,
|
|
14719
|
+
sessionRef: input.sessionRef,
|
|
14720
|
+
runtimeId: input.runtimeId,
|
|
14721
|
+
cwd: binding.resolution.canonicalHome
|
|
14722
|
+
});
|
|
14723
|
+
if (handoff.taskId !== input.taskId) {
|
|
14724
|
+
throw new Error("Agent reliable egress taskId does not match the durable session handoff");
|
|
14725
|
+
}
|
|
14631
14726
|
const appended = await agentEgress.appendReliable({
|
|
14632
14727
|
homeDir: binding.resolution.canonicalHome,
|
|
14633
14728
|
agentRef: binding.resolution.agentRef,
|
|
14634
14729
|
sessionRef: input.sessionRef,
|
|
14635
14730
|
payload: input.payload,
|
|
14636
|
-
|
|
14731
|
+
taskId: input.taskId,
|
|
14637
14732
|
...input.eventId === void 0 ? {} : { eventId: input.eventId }
|
|
14638
14733
|
});
|
|
14639
14734
|
if (appended.ok) dispatchReliableRecord(appended.record);
|