@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.
@@ -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
  /**
@@ -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
- const parsed = JSON.parse(raw);
4697
- if (typeof parsed.deviceId === "string" && typeof parsed.accessToken === "string" && typeof parsed.expiresAt === "string" && typeof parsed.devicePrivateKeyPem === "string" && typeof parsed.devicePublicKey === "string") {
4698
- return {
4699
- deviceId: parsed.deviceId,
4700
- accessToken: parsed.accessToken,
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
- return void 0;
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
- const existing = this.record ?? await this.opts.store.load();
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 (payload.sessionRef !== void 0) {
10349
+ if (sessionRef !== void 0) {
10311
10350
  try {
10312
10351
  await this.deps.agentSessionHandoffs.requireMatch({
10313
10352
  agentRef: agentBinding.resolution.agentRef,
10314
- sessionRef: payload.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 = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
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 (payload.sessionRef) {
10340
- const ledger = await gitStore.findBySessionAnyPhase(payload.sessionRef).catch(() => void 0);
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 !== payload.sessionRef || path.resolve(ledger.workspaceDir) !== path.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
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, payload.sessionRef);
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 = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
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
- ...payload.sessionRef === void 0 || known === void 0 && agentBinding === void 0 ? {} : { sessionRef: payload.sessionRef },
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: payload.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) flags.push(AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY);
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
- const agentEgress = new AgentEgressController({
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
- const previous = await store.load();
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: config.hostedJournal.tenantId, productId: config.productId, deviceId: record.deviceId } : void 0;
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
- const tenantId2 = config.agentEgress?.tenantId;
13976
- if (tenantId2 === void 0) return true;
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: config.agentEgress.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
- ...input.taskId === void 0 ? {} : { taskId: input.taskId },
14731
+ taskId: input.taskId,
14637
14732
  ...input.eventId === void 0 ? {} : { eventId: input.eventId }
14638
14733
  });
14639
14734
  if (appended.ok) dispatchReliableRecord(appended.record);