@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 CHANGED
@@ -122,17 +122,19 @@ contents.
122
122
 
123
123
  ## Agent egress and explicit content reads
124
124
 
125
- `agentEgress` is consumed configuration, not a profile projection. The host
126
- must select one exact policy revision and an authenticated tenant id. Omitting
127
- contentful mode keeps runtime activity metadata/status-only; enabling it is an
128
- explicit product decision and requires the server capability. Reliable events
129
- are fsynced under the canonical Agent home and retire only after an exact ack.
125
+ `agentEgress` is consumed policy configuration, not a profile or tenant
126
+ projection. The host selects one exact policy revision. The daemon obtains its
127
+ tenant binding only from the authenticated pair response persisted in the
128
+ atomic local `DeviceRecord`; there is no `agentEgress.tenantId` setting and no
129
+ Profile/config, deviceId, or access-token fallback. Omitting contentful mode
130
+ keeps runtime activity metadata/status-only; enabling it is an explicit product
131
+ decision and requires the server capability. Reliable events are fsynced under
132
+ the canonical Agent home and retire only after an exact ack.
130
133
 
131
134
  ```ts
132
135
  createDaemon({
133
136
  // ...normal device, transport and agentHome configuration
134
137
  agentEgress: {
135
- tenantId: 'tenant-authority-from-salesko',
136
138
  policy: {
137
139
  policyRevision: 'salesko-agent-egress-r1',
138
140
  activity: { mode: 'metadata-status', delivery: 'latest-value' },
@@ -163,8 +165,9 @@ its local supplement. The local supplement can only narrow root, text, MIME,
163
165
  size and sensitive-name behavior; it cannot enable a wire-disabled surface.
164
166
  The SDK derives `agents/<agentId>`, `.byok/egress`, runtime-session evidence and
165
167
  the per-Agent content-read audit path. Salesko must not compose those paths.
166
- Tenant/device identity comes from authenticated host state; a request cannot
167
- override it. Transcript reads additionally require the exact persisted
168
+ Tenant/device identity comes from the persisted authenticated enrollment; a
169
+ request or editable host configuration cannot override it. Transcript reads
170
+ additionally require the exact persisted
168
171
  AgentRef/session/runtime/cwd handoff. Allowed content is uploaded through the
169
172
  authenticated blob channel. The content-free receipt is fsynced into the
170
173
  Agent-local reliable spool with stable event/cursor identity before send and
@@ -2,10 +2,10 @@
2
2
  import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, createPrivateKey, generateKeyPairSync, sign } from 'crypto';
3
3
  import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
4
4
  import path, { isAbsolute, join } from 'path';
5
- import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, 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';
5
+ import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, 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';
6
6
  import { execFile, spawn } from 'child_process';
7
7
  import os from 'os';
8
- import { DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
8
+ import { isTenantId, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
9
9
  import { promisify } from 'util';
10
10
  import { fileURLToPath } from 'url';
11
11
  import 'readline';
@@ -4732,6 +4732,13 @@ var ApprovalRegistry = class {
4732
4732
  }
4733
4733
  };
4734
4734
  var MAX_DEVICE_RECORD_BYTES = 256 * 1024;
4735
+ var REPAIR_REQUIRED_MESSAGE = "device enrollment record is missing or has an invalid authenticated tenant binding; re-pair required";
4736
+ var DeviceRecordRePairRequiredError = class extends Error {
4737
+ constructor() {
4738
+ super(REPAIR_REQUIRED_MESSAGE);
4739
+ this.name = "DeviceRecordRePairRequiredError";
4740
+ }
4741
+ };
4735
4742
  function sameInode(left, right) {
4736
4743
  return left.dev === right.dev && left.ino === right.ino;
4737
4744
  }
@@ -4741,18 +4748,32 @@ function sameFileState(left, right) {
4741
4748
  function sameContentState(left, right) {
4742
4749
  return sameInode(left, right) && left.size === right.size && left.mtimeNs === right.mtimeNs;
4743
4750
  }
4751
+ function assertDeviceRecord(value) {
4752
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4753
+ throw new DeviceRecordRePairRequiredError();
4754
+ }
4755
+ const parsed = value;
4756
+ if (typeof parsed.deviceId === "string" && isTenantId(parsed.tenantId) && typeof parsed.accessToken === "string" && typeof parsed.expiresAt === "string" && typeof parsed.devicePrivateKeyPem === "string" && typeof parsed.devicePublicKey === "string") {
4757
+ return;
4758
+ }
4759
+ throw new DeviceRecordRePairRequiredError();
4760
+ }
4744
4761
  function parseDeviceRecord(raw) {
4745
- const parsed = JSON.parse(raw);
4746
- if (typeof parsed.deviceId === "string" && typeof parsed.accessToken === "string" && typeof parsed.expiresAt === "string" && typeof parsed.devicePrivateKeyPem === "string" && typeof parsed.devicePublicKey === "string") {
4747
- return {
4748
- deviceId: parsed.deviceId,
4749
- accessToken: parsed.accessToken,
4750
- expiresAt: parsed.expiresAt,
4751
- devicePrivateKeyPem: parsed.devicePrivateKeyPem,
4752
- devicePublicKey: parsed.devicePublicKey
4753
- };
4762
+ let parsed;
4763
+ try {
4764
+ parsed = JSON.parse(raw);
4765
+ } catch {
4766
+ throw new DeviceRecordRePairRequiredError();
4754
4767
  }
4755
- return void 0;
4768
+ assertDeviceRecord(parsed);
4769
+ return {
4770
+ deviceId: parsed.deviceId,
4771
+ tenantId: parsed.tenantId,
4772
+ accessToken: parsed.accessToken,
4773
+ expiresAt: parsed.expiresAt,
4774
+ devicePrivateKeyPem: parsed.devicePrivateKeyPem,
4775
+ devicePublicKey: parsed.devicePublicKey
4776
+ };
4756
4777
  }
4757
4778
  var DeviceStore = class _DeviceStore {
4758
4779
  /**
@@ -4822,6 +4843,7 @@ var DeviceStore = class _DeviceStore {
4822
4843
  }
4823
4844
  }
4824
4845
  async save(record) {
4846
+ assertDeviceRecord(record);
4825
4847
  const storeDir = path.dirname(this.filePath);
4826
4848
  await ensureSecureDir(storeDir, this.secureDirOptions);
4827
4849
  await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
@@ -4992,7 +5014,14 @@ var AuthManager = class {
4992
5014
  this.proactiveTimer = void 0;
4993
5015
  try {
4994
5016
  return await this.runCredentialMutation(async () => {
4995
- const existing = this.record ?? await this.opts.store.load();
5017
+ let existing = this.record;
5018
+ if (!existing) {
5019
+ try {
5020
+ existing = await this.opts.store.load();
5021
+ } catch (error) {
5022
+ if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
5023
+ }
5024
+ }
4996
5025
  const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
4997
5026
  const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
4998
5027
  const res = await fetch(url, {
@@ -5007,9 +5036,10 @@ var AuthManager = class {
5007
5036
  if (!res.ok) {
5008
5037
  throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
5009
5038
  }
5010
- const body = await res.json();
5039
+ const body = PairResponseSchema.parse(await res.json());
5011
5040
  const record = {
5012
5041
  deviceId: body.deviceId,
5042
+ tenantId: body.tenantId,
5013
5043
  accessToken: body.accessToken,
5014
5044
  expiresAt: resolvePairExpiry(body.refreshHint),
5015
5045
  devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey),
@@ -9880,6 +9910,11 @@ function offeredAgentRef(payload) {
9880
9910
  if (!Object.prototype.hasOwnProperty.call(payload, "agentRef")) return void 0;
9881
9911
  return validateAgentRef(payload.agentRef);
9882
9912
  }
9913
+ function offeredSessionRef(payload) {
9914
+ if (!Object.prototype.hasOwnProperty.call(payload, "sessionRef")) return void 0;
9915
+ const value = payload.sessionRef;
9916
+ return typeof value === "string" ? value : void 0;
9917
+ }
9883
9918
  function errorMessage4(err) {
9884
9919
  return err instanceof Error ? err.message : String(err);
9885
9920
  }
@@ -10248,6 +10283,9 @@ var TaskRunner = class {
10248
10283
  case "task.offer_for_agent_with_egress":
10249
10284
  await this.handleOffer(envelope.task_id, envelope.payload, true);
10250
10285
  return;
10286
+ case "task.offer_for_agent_with_egress_fresh":
10287
+ await this.handleOffer(envelope.task_id, envelope.payload, true);
10288
+ return;
10251
10289
  case "task.cancel":
10252
10290
  await this.handleCancel(envelope.task_id, envelope.payload.reason);
10253
10291
  return;
@@ -10289,6 +10327,7 @@ var TaskRunner = class {
10289
10327
  const decline = (reason, retryable) => {
10290
10328
  this.decline(taskId, reason, retryable, agentRef);
10291
10329
  };
10330
+ const sessionRef = offeredSessionRef(payload);
10292
10331
  if ("egressPolicy" in payload) {
10293
10332
  if (this.deps.agentEgressPolicy === void 0 || !sameEgressPolicy(this.deps.agentEgressPolicy, payload.egressPolicy)) {
10294
10333
  decline("Agent egress offer policy is not exactly enabled by this daemon", false);
@@ -10390,11 +10429,11 @@ var TaskRunner = class {
10390
10429
  let plainWorkspaceNeedsResolve = false;
10391
10430
  if (agentBinding !== void 0) {
10392
10431
  workspaceDir = agentBinding.lease.cwd;
10393
- if (payload.sessionRef !== void 0) {
10432
+ if (sessionRef !== void 0) {
10394
10433
  try {
10395
10434
  await this.deps.agentSessionHandoffs.requireMatch({
10396
10435
  agentRef: agentBinding.resolution.agentRef,
10397
- sessionRef: payload.sessionRef,
10436
+ sessionRef,
10398
10437
  runtimeId: pick.descriptor.id,
10399
10438
  cwd: workspaceDir
10400
10439
  });
@@ -10416,15 +10455,15 @@ var TaskRunner = class {
10416
10455
  return;
10417
10456
  }
10418
10457
  } else if (this.deps.gitWorkspaceManager && this.deps.gitWorkspaceStore) {
10419
- known = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
10458
+ known = sessionRef ? await this.deps.sessionWorkspaces.get(sessionRef) : void 0;
10420
10459
  const gitManager = this.deps.gitWorkspaceManager;
10421
10460
  const gitStore = this.deps.gitWorkspaceStore;
10422
- if (payload.sessionRef) {
10423
- const ledger = await gitStore.findBySessionAnyPhase(payload.sessionRef).catch(() => void 0);
10461
+ if (sessionRef) {
10462
+ const ledger = await gitStore.findBySessionAnyPhase(sessionRef).catch(() => void 0);
10424
10463
  const sameProtocolTask = ledger?.taskId === taskId;
10425
10464
  const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
10426
10465
  const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
10427
- 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) {
10466
+ 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) {
10428
10467
  decline("session is incompatible with Git workspace mode", true);
10429
10468
  return;
10430
10469
  }
@@ -10443,13 +10482,13 @@ var TaskRunner = class {
10443
10482
  gitWorkspaceId = randomUUID();
10444
10483
  }
10445
10484
  try {
10446
- gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
10485
+ gitLease = await gitManager.acquireLease(workspaceDir, sessionRef);
10447
10486
  } catch {
10448
10487
  decline("workspace is busy or unavailable", true);
10449
10488
  return;
10450
10489
  }
10451
10490
  } else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
10452
- known = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
10491
+ known = sessionRef ? await this.deps.sessionWorkspaces.get(sessionRef) : void 0;
10453
10492
  workspaceDir = known?.workspaceDir ?? path.join(this.deps.workspaceRoot, taskId);
10454
10493
  plainWorkspaceNeedsResolve = true;
10455
10494
  } else {
@@ -10468,7 +10507,7 @@ var TaskRunner = class {
10468
10507
  policy: decision.policy,
10469
10508
  requiredToolsetIds: requiredToolsets ?? [],
10470
10509
  ...offered.dispatchSelection === void 0 ? {} : { dispatchSelection: offered.dispatchSelection },
10471
- ...payload.sessionRef === void 0 || known === void 0 && agentBinding === void 0 ? {} : { sessionRef: payload.sessionRef },
10510
+ ...sessionRef === void 0 || known === void 0 && agentBinding === void 0 ? {} : { sessionRef },
10472
10511
  ...agentBinding === void 0 ? {} : {
10473
10512
  agentRef: agentBinding.resolution.agentRef,
10474
10513
  cwd: agentBinding.lease.cwd,
@@ -10571,7 +10610,7 @@ var TaskRunner = class {
10571
10610
  workspaceId,
10572
10611
  taskId,
10573
10612
  workspaceDir,
10574
- sessionRef: payload.sessionRef,
10613
+ sessionRef,
10575
10614
  phase,
10576
10615
  baseline: gitBaseline ?? observation.head,
10577
10616
  current: observation.head,
@@ -12428,9 +12467,14 @@ var AgentEgressController = class {
12428
12467
  latestStatus = emptyLane();
12429
12468
  reliableStatus = emptyLane();
12430
12469
  drops = [];
12470
+ active = true;
12431
12471
  get policy() {
12432
12472
  return this.options.policy;
12433
12473
  }
12474
+ /** Permanently fail closed after its authenticated enrollment is replaced. */
12475
+ deactivate() {
12476
+ this.active = false;
12477
+ }
12434
12478
  status() {
12435
12479
  const reliable = this.reliableRecords();
12436
12480
  return Object.freeze({
@@ -12448,6 +12492,10 @@ var AgentEgressController = class {
12448
12492
  /** Project before TaskRunner builds a `task.progress` envelope. */
12449
12493
  projectLatestValue(input) {
12450
12494
  if (input.agentRef === void 0) return Object.freeze([...input.events]);
12495
+ if (!this.active) {
12496
+ this.noteDrop("latest-value", "policy_denied", input.agentRef);
12497
+ return [];
12498
+ }
12451
12499
  if (this.options.policy.activity.mode === "contentful-trajectory" && !input.serverCapabilities.includes("agent-egress-policy")) {
12452
12500
  this.noteDrop("latest-value", "capability_missing", input.agentRef);
12453
12501
  return [];
@@ -12470,7 +12518,7 @@ var AgentEgressController = class {
12470
12518
  return latest === void 0 ? [] : Object.freeze([latest]);
12471
12519
  }
12472
12520
  async appendReliable(input) {
12473
- if (this.options.tenantId === void 0) {
12521
+ if (!this.active || this.options.tenantId === void 0) {
12474
12522
  this.noteDrop("reliable", "policy_denied", input.agentRef);
12475
12523
  return { ok: false, reason: "policy_denied" };
12476
12524
  }
@@ -12508,7 +12556,7 @@ var AgentEgressController = class {
12508
12556
  * with `wireType: agent.content.receipt` before any transport attempt.
12509
12557
  */
12510
12558
  async appendContentReceipt(input) {
12511
- if (this.options.tenantId === void 0) {
12559
+ if (!this.active || this.options.tenantId === void 0) {
12512
12560
  this.noteDrop("reliable", "policy_denied", input.agentRef);
12513
12561
  return { ok: false, reason: "policy_denied" };
12514
12562
  }
@@ -12532,7 +12580,7 @@ var AgentEgressController = class {
12532
12580
  /** Retires only the record whose full Agent/tenant/revision/id/cursor tuple matches. */
12533
12581
  async acknowledge(ack) {
12534
12582
  const spool = this.spools.get(agentKey(ack.agentRef));
12535
- if (!spool || this.options.tenantId === void 0 || ack.tenantId !== this.options.tenantId) {
12583
+ if (!this.active || !spool || this.options.tenantId === void 0 || ack.tenantId !== this.options.tenantId) {
12536
12584
  this.noteDrop("reliable", "ack_mismatch", ack.agentRef);
12537
12585
  return false;
12538
12586
  }
@@ -12543,6 +12591,7 @@ var AgentEgressController = class {
12543
12591
  /** Re-open every existing Agent-local spool before retrying stable records after restart. */
12544
12592
  async recover(agentsRoot) {
12545
12593
  if (!path.isAbsolute(agentsRoot)) throw new Error("Agent egress recovery root must be absolute");
12594
+ if (!this.active) throw new Error("Agent egress recovery requires an active authenticated enrollment");
12546
12595
  if (this.options.tenantId === void 0) {
12547
12596
  throw new Error("Agent egress recovery requires one authenticated tenant authority");
12548
12597
  }
@@ -13528,7 +13577,13 @@ function computeCapabilities(adapters, agentHomeConfigured = false, agentEgressC
13528
13577
  flags.push("toolset-selection");
13529
13578
  }
13530
13579
  if (agentHomeConfigured) flags.push("agent-home-contract");
13531
- if (agentEgressConfigured) flags.push(AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY);
13580
+ if (agentEgressConfigured) {
13581
+ flags.push(
13582
+ AGENT_EGRESS_POLICY_CAPABILITY,
13583
+ AGENT_EGRESS_RELIABLE_ACK_CAPABILITY,
13584
+ AGENT_EGRESS_FRESH_SESSION_CAPABILITY
13585
+ );
13586
+ }
13532
13587
  if (contentReadPolicies !== void 0) {
13533
13588
  for (const surface of Object.keys(AGENT_CONTENT_READ_CAPABILITIES)) {
13534
13589
  const policy = contentReadPolicies[surface];
@@ -13650,9 +13705,6 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13650
13705
  if (config.agentEgress !== void 0 && config.agentHome === void 0) {
13651
13706
  throw new Error("DaemonConfig.agentEgress requires DaemonConfig.agentHome for the per-Agent local spool");
13652
13707
  }
13653
- if (config.agentEgress !== void 0 && (config.agentEgress.tenantId.length === 0 || config.agentEgress.tenantId.trim() !== config.agentEgress.tenantId)) {
13654
- throw new Error("DaemonConfig.agentEgress.tenantId must be a non-empty canonical authenticated tenant id");
13655
- }
13656
13708
  const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
13657
13709
  const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
13658
13710
  ...config.progressBatch,
@@ -13683,9 +13735,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13683
13735
  });
13684
13736
  const agentSessionHandoffs = config.agentHome === void 0 ? void 0 : new AgentSessionHandoffStore();
13685
13737
  const agentContentReadPolicies = resolveContentReadPolicies(egressPolicy, config.agentEgress?.contentRead);
13686
- const agentEgress = new AgentEgressController({
13738
+ let agentEgress = new AgentEgressController({
13687
13739
  policy: egressPolicy,
13688
- ...config.agentEgress?.tenantId === void 0 ? {} : { tenantId: config.agentEgress.tenantId },
13689
13740
  ...config.agentEgress?.sanitizer === void 0 ? {} : { sanitizer: config.agentEgress.sanitizer }
13690
13741
  });
13691
13742
  const gitWorkspaceManager = config.gitWorkspace ? overrides.gitWorkspace?.manager ?? new GitWorkspaceManager(config.workspaceRoot, { ownerId: stableGitWorkspaceOwnerId(storeDir, config.productId) }) : void 0;
@@ -13695,11 +13746,6 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13695
13746
  if (config.hostedJournal.mode !== "sqlite") {
13696
13747
  throw new Error(`DaemonConfig.hostedJournal.mode must be "sqlite" \u2014 got ${JSON.stringify(config.hostedJournal.mode)}`);
13697
13748
  }
13698
- if (typeof config.hostedJournal.tenantId !== "string" || config.hostedJournal.tenantId.trim() === "") {
13699
- throw new Error(
13700
- "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"
13701
- );
13702
- }
13703
13749
  if (config.hostedJournal.storagePolicy) {
13704
13750
  resolvedStoragePolicy = resolveLocalStoragePolicy(config.hostedJournal.storagePolicy);
13705
13751
  }
@@ -13768,6 +13814,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13768
13814
  const approvalRegistry = new ApprovalRegistry();
13769
13815
  let connection;
13770
13816
  let connectionState = "closed";
13817
+ let daemonStarted = false;
13818
+ let tenantRebinding = false;
13771
13819
  let runner;
13772
13820
  let controlServerHandle;
13773
13821
  let daemonOwnerLease;
@@ -13820,11 +13868,31 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13820
13868
  return runLifecycleMutation(() => pairUnderLease(pairingCode));
13821
13869
  }
13822
13870
  async function pairUnderLease(pairingCode) {
13871
+ const wasRunning = daemonStarted;
13872
+ if (wasRunning) tenantRebinding = true;
13823
13873
  const acquiredHere = daemonOwnerLease === void 0;
13824
13874
  if (acquiredHere) daemonOwnerLease = await acquireDaemonOwner(storeDir, "daemon");
13875
+ let replacementPersisted = false;
13825
13876
  try {
13826
- const previous = await store.load();
13877
+ let previous;
13878
+ try {
13879
+ previous = await store.load();
13880
+ } catch (error) {
13881
+ if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
13882
+ }
13827
13883
  const record = await auth.pair(pairingCode);
13884
+ replacementPersisted = true;
13885
+ if (config.agentEgress !== void 0) {
13886
+ agentEgress.deactivate();
13887
+ agentEgress = new AgentEgressController({
13888
+ policy: egressPolicy,
13889
+ ...config.agentEgress.sanitizer === void 0 ? {} : { sanitizer: config.agentEgress.sanitizer }
13890
+ });
13891
+ }
13892
+ if (wasRunning) {
13893
+ await runShutdownSequence("re-pairing enrollment binding");
13894
+ tenantRebinding = false;
13895
+ }
13828
13896
  if (previous && previous.deviceId !== record.deviceId) {
13829
13897
  await cursorStore.clear(config.serverUrl, previous.deviceId);
13830
13898
  }
@@ -13835,6 +13903,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13835
13903
  }
13836
13904
  return record;
13837
13905
  } catch (err) {
13906
+ if (wasRunning && !replacementPersisted) tenantRebinding = false;
13838
13907
  if (acquiredHere) {
13839
13908
  await daemonOwnerLease?.release();
13840
13909
  daemonOwnerLease = void 0;
@@ -13857,6 +13926,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13857
13926
  if (!record) {
13858
13927
  throw new Error("device is not paired yet; call pair(pairingCode) first");
13859
13928
  }
13929
+ if (config.agentEgress !== void 0) {
13930
+ agentEgress = new AgentEgressController({
13931
+ policy: egressPolicy,
13932
+ tenantId: record.tenantId,
13933
+ ...config.agentEgress.sanitizer === void 0 ? {} : { sanitizer: config.agentEgress.sanitizer }
13934
+ });
13935
+ }
13860
13936
  await agentHomeManager?.preflight();
13861
13937
  if (config.agentEgress !== void 0 && config.agentHome !== void 0) {
13862
13938
  await agentEgress.recover(path.join(config.agentHome.hostStorageRoot, "agents"));
@@ -13917,7 +13993,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
13917
13993
  config.agentEgress !== void 0,
13918
13994
  agentContentReadPolicies
13919
13995
  );
13920
- const journalIdentity = config.hostedJournal ? { tenantId: config.hostedJournal.tenantId, productId: config.productId, deviceId: record.deviceId } : void 0;
13996
+ const journalIdentity = config.hostedJournal ? { tenantId: record.tenantId, productId: config.productId, deviceId: record.deviceId } : void 0;
13921
13997
  const sendSanitizedEnvelope = activeJournal && journalIdentity ? (envelope) => {
13922
13998
  observer.handleOutboundEnvelope(envelope);
13923
13999
  const terminalKind = terminalKindOf(envelope.type);
@@ -14055,9 +14131,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
14055
14131
  runner = new TaskRunner(deps);
14056
14132
  const handleAgentEgressEnvelope = async (envelope) => {
14057
14133
  if (envelope.type !== "agent.egress.ack") return false;
14058
- const tenantId = config.agentEgress?.tenantId;
14059
- if (tenantId === void 0) return true;
14060
- await agentEgress.acknowledge({ ...envelope.payload, tenantId });
14134
+ if (config.agentEgress === void 0) return true;
14135
+ await agentEgress.acknowledge({ ...envelope.payload, tenantId: record.tenantId });
14061
14136
  return true;
14062
14137
  };
14063
14138
  const handleAgentContentReadEnvelope = async (envelope) => {
@@ -14109,7 +14184,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
14109
14184
  const result = await policyEngine.read({
14110
14185
  requestId: payload.requestId,
14111
14186
  actor: payload.actor,
14112
- tenantId: config.agentEgress.tenantId,
14187
+ tenantId: record.tenantId,
14113
14188
  deviceId: record.deviceId,
14114
14189
  agentRef: payload.agentRef,
14115
14190
  surface: payload.surface,
@@ -14205,6 +14280,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
14205
14280
  //
14206
14281
  // The no-journal branch is the ORIGINAL closure, unchanged.
14207
14282
  onEnvelope: activeJournal && journalIdentity ? async (envelope) => {
14283
+ if (tenantRebinding) {
14284
+ throw new Error("tenant enrollment is being re-paired; inbound work is blocked until restart");
14285
+ }
14208
14286
  observer.handleInboundEnvelope(envelope);
14209
14287
  if (await handleAgentEgressEnvelope(envelope)) return;
14210
14288
  if (await handleAgentContentReadEnvelope(envelope)) return;
@@ -14212,6 +14290,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
14212
14290
  await activeJournal.appendEnvelope(toJournalEnvelopeRecord(envelope, journalIdentity));
14213
14291
  return runner?.handleEnvelope(envelope) ?? Promise.resolve();
14214
14292
  } : (envelope) => {
14293
+ if (tenantRebinding) {
14294
+ return Promise.reject(new Error("tenant enrollment is being re-paired; inbound work is blocked until restart"));
14295
+ }
14215
14296
  observer.handleInboundEnvelope(envelope);
14216
14297
  if (envelope.type === "agent.egress.ack") return handleAgentEgressEnvelope(envelope).then(() => void 0);
14217
14298
  if (envelope.type === "agent.content.read") return handleAgentContentReadEnvelope(envelope).then(() => void 0);
@@ -14243,6 +14324,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
14243
14324
  dispatchReliableRecord(record2);
14244
14325
  }
14245
14326
  startPresenceProducer();
14327
+ daemonStarted = true;
14246
14328
  } catch (err) {
14247
14329
  try {
14248
14330
  await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
@@ -14387,6 +14469,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
14387
14469
  if (!mutationBarrierComplete) {
14388
14470
  throw new Error("daemon shutdown mutation barrier is incomplete; ownership lease retained");
14389
14471
  }
14472
+ daemonStarted = false;
14390
14473
  }
14391
14474
  async function stop(opts = {}) {
14392
14475
  shuttingDown = true;
@@ -14705,18 +14788,30 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
14705
14788
  connection.send(sanitized.envelope);
14706
14789
  }
14707
14790
  async function publishReliableAgentEgress(input) {
14708
- if (config.agentEgress === void 0 || agentHomeManager === void 0) {
14791
+ if (config.agentEgress === void 0 || agentHomeManager === void 0 || agentSessionHandoffs === void 0) {
14709
14792
  throw new Error("Agent reliable egress is not configured");
14710
14793
  }
14794
+ if (tenantRebinding) {
14795
+ throw new Error("tenant enrollment is being re-paired; reliable egress is blocked until restart");
14796
+ }
14711
14797
  const binding = await agentHomeManager.acquire(input.agentRef);
14712
14798
  try {
14713
14799
  await agentHomeManager.initialize(binding);
14800
+ const handoff = await agentSessionHandoffs.requireMatch({
14801
+ agentRef: binding.resolution.agentRef,
14802
+ sessionRef: input.sessionRef,
14803
+ runtimeId: input.runtimeId,
14804
+ cwd: binding.resolution.canonicalHome
14805
+ });
14806
+ if (handoff.taskId !== input.taskId) {
14807
+ throw new Error("Agent reliable egress taskId does not match the durable session handoff");
14808
+ }
14714
14809
  const appended = await agentEgress.appendReliable({
14715
14810
  homeDir: binding.resolution.canonicalHome,
14716
14811
  agentRef: binding.resolution.agentRef,
14717
14812
  sessionRef: input.sessionRef,
14718
14813
  payload: input.payload,
14719
- ...input.taskId === void 0 ? {} : { taskId: input.taskId },
14814
+ taskId: input.taskId,
14720
14815
  ...input.eventId === void 0 ? {} : { eventId: input.eventId }
14721
14816
  });
14722
14817
  if (appended.ok) dispatchReliableRecord(appended.record);
@@ -15381,7 +15476,7 @@ function createServiceLifecycle(def, opts = {}) {
15381
15476
 
15382
15477
  // src/bin/official-release.ts
15383
15478
  var OFFICIAL_LOCAL_AGENT_RELEASE = resolveLocalAgentReleaseIdentity({
15384
- version: "0.6.1"
15479
+ version: "0.8.0-beta.0"
15385
15480
  });
15386
15481
 
15387
15482
  // src/bin/config.ts