farai 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -5707,33 +5707,67 @@ var init_docker_environment = __esm(() => {
5707
5707
 
5708
5708
  // src/agent-container/lifecycle.ts
5709
5709
  import { createHash as createHash3 } from "crypto";
5710
+ import { statSync as statSync2 } from "fs";
5710
5711
  import { join as join8, resolve as resolve3 } from "path";
5711
5712
  import { Database as Database2 } from "bun:sqlite";
5712
5713
 
5713
5714
  class DockerContainerLifecycle {
5714
5715
  acquired = new Map;
5716
+ stopFailures = new Set;
5717
+ activeOperations = 0;
5715
5718
  lastReconcileAt = 0;
5716
5719
  disposed = false;
5720
+ suspending = false;
5717
5721
  constructor(runtimeId, runner = runDockerProcess, options = {}) {
5718
5722
  this.runtimeId = runtimeId;
5719
5723
  this.runner = runner;
5720
5724
  this.registry = new ContainerLeaseRegistry(options.registryPath);
5721
5725
  this.idleTtlMs = options.idleTtlMs ?? CONTAINER_IDLE_TTL_MS;
5726
+ this.reconcileTimer = setInterval(() => {
5727
+ if (this.disposed || this.suspending)
5728
+ return;
5729
+ this.reconcile().catch(() => {
5730
+ return;
5731
+ });
5732
+ }, CONTAINER_RECONCILE_INTERVAL_MS);
5733
+ this.reconcileTimer.unref?.();
5722
5734
  }
5723
5735
  async acquire(identity) {
5724
5736
  this.assertOpen();
5725
- if (Date.now() - this.lastReconcileAt >= 60000)
5737
+ this.beginOperation();
5738
+ try {
5739
+ await this.acquireUnlocked(identity);
5740
+ } finally {
5741
+ this.endOperation();
5742
+ }
5743
+ }
5744
+ async withLease(identity, operation) {
5745
+ this.assertOpen();
5746
+ this.beginOperation();
5747
+ try {
5748
+ await this.acquireUnlocked(identity);
5749
+ this.assertOpen();
5750
+ return await operation();
5751
+ } finally {
5752
+ this.endOperation();
5753
+ }
5754
+ }
5755
+ async acquireUnlocked(identity) {
5756
+ if (Date.now() - this.lastReconcileAt >= CONTAINER_RECONCILE_INTERVAL_MS)
5726
5757
  await this.reconcile();
5727
5758
  else
5728
5759
  await this.reconcilePromise;
5760
+ this.assertOpen();
5729
5761
  this.registry.acquire(identity, this.runtimeId, CONTAINER_LEASE_MS);
5730
5762
  this.acquired.set(identity.containerName, identity);
5763
+ this.stopFailures.delete(identity.containerName);
5731
5764
  }
5732
5765
  release(identity) {
5733
5766
  if (this.disposed)
5734
5767
  return;
5735
5768
  this.registry.release(identity.containerName, this.runtimeId);
5736
5769
  this.acquired.delete(identity.containerName);
5770
+ this.stopFailures.delete(identity.containerName);
5737
5771
  }
5738
5772
  renew() {
5739
5773
  if (this.disposed || this.acquired.size === 0)
@@ -5744,8 +5778,10 @@ class DockerContainerLifecycle {
5744
5778
  this.assertOpen();
5745
5779
  if (this.reconcilePromise)
5746
5780
  return this.reconcilePromise;
5747
- this.reconcilePromise = this.performReconcile().finally(() => {
5748
- this.lastReconcileAt = Date.now();
5781
+ this.reconcilePromise = this.performReconcile().then((completed) => {
5782
+ if (completed)
5783
+ this.lastReconcileAt = Date.now();
5784
+ }).finally(() => {
5749
5785
  this.reconcilePromise = undefined;
5750
5786
  });
5751
5787
  return this.reconcilePromise;
@@ -5753,83 +5789,285 @@ class DockerContainerLifecycle {
5753
5789
  async performReconcile() {
5754
5790
  const listed = await listManagedContainers(this.runner);
5755
5791
  if (!listed)
5756
- return;
5792
+ return false;
5757
5793
  const names = new Set(listed.map((container) => container.containerName));
5758
- const toStop = new Set;
5794
+ const toStop = new Map;
5795
+ const toRemove = new Map;
5759
5796
  for (const container of listed) {
5760
- if (this.registry.has(container.containerName))
5761
- continue;
5762
5797
  this.registry.adopt(container);
5763
- if (container.state === "running")
5764
- toStop.add(container.containerName);
5798
+ if (this.workspaceIsOrphaned(container.workspace) && this.registry.claimUnleased(container, this.runtimeId, CONTAINER_LEASE_MS)) {
5799
+ if (containerNeedsStop(container.state))
5800
+ toStop.set(container.containerName, container);
5801
+ else
5802
+ toRemove.set(container.containerName, container);
5803
+ } else if (containerNeedsStop(container.state) && this.registry.claimUnleased(container, this.runtimeId, CONTAINER_LEASE_MS)) {
5804
+ toStop.set(container.containerName, container);
5805
+ }
5765
5806
  }
5766
5807
  this.registry.deleteMissing(names);
5767
- const abandoned = this.registry.expireLeases();
5808
+ const abandoned = this.registry.expireLeases(this.runtimeId, CONTAINER_LEASE_MS);
5768
5809
  for (const identity of abandoned) {
5769
5810
  const container = listed.find((candidate) => candidate.containerName === identity.containerName);
5770
- if (container?.state === "running")
5771
- toStop.add(identity.containerName);
5811
+ if (container && managedContainerIdentityMatches(container, identity)) {
5812
+ if (this.workspaceIsOrphaned(container.workspace))
5813
+ toRemove.set(identity.containerName, identity);
5814
+ else if (containerNeedsStop(container.state))
5815
+ toStop.set(identity.containerName, identity);
5816
+ else
5817
+ toRemove.set(identity.containerName, identity);
5818
+ } else {
5819
+ this.registry.delete(identity.containerName, this.runtimeId);
5820
+ }
5772
5821
  }
5773
- await Promise.allSettled([...toStop].map((name) => this.runner("docker", ["stop", "-t", "1", name])));
5822
+ await Promise.allSettled([...toStop.values()].map((identity) => this.stopIfOwned(identity)));
5774
5823
  const claimed = this.registry.claimExpiredIdle(this.runtimeId, this.idleTtlMs);
5775
- await Promise.all(claimed.map(async (identity) => {
5776
- const result = await this.runner("docker", ["rm", "-f", "-v", identity.containerName]);
5824
+ for (const identity of claimed)
5825
+ toRemove.set(identity.containerName, identity);
5826
+ await Promise.all([...toRemove.values()].map((identity) => this.removeIfOwned(identity)));
5827
+ return true;
5828
+ }
5829
+ workspaceIsOrphaned(workspace) {
5830
+ try {
5831
+ return !statSync2(workspace).isDirectory();
5832
+ } catch {
5833
+ return true;
5834
+ }
5835
+ }
5836
+ async stopIfOwned(identity) {
5837
+ let release = true;
5838
+ try {
5839
+ const inspected = await this.inspectContainer(identity.containerName);
5840
+ if (inspected.error) {
5841
+ release = false;
5842
+ return;
5843
+ }
5844
+ if (!inspected.exists || !managedContainerBelongsTo(inspected.labels, identity)) {
5845
+ this.registry.delete(identity.containerName, this.runtimeId);
5846
+ return;
5847
+ }
5848
+ await this.runner("docker", ["stop", "-t", "1", identity.containerName], {
5849
+ timeoutMs: 1250
5850
+ });
5851
+ } catch {} finally {
5852
+ if (release)
5853
+ this.registry.release(identity.containerName, this.runtimeId);
5854
+ }
5855
+ }
5856
+ async removeIfOwned(identity) {
5857
+ try {
5858
+ const inspected = await this.inspectContainer(identity.containerName);
5859
+ if (inspected.error)
5860
+ return;
5861
+ if (!inspected.exists || !managedContainerBelongsTo(inspected.labels, identity)) {
5862
+ this.registry.delete(identity.containerName, this.runtimeId);
5863
+ return;
5864
+ }
5865
+ const result = await this.runner("docker", ["rm", "-f", "-v", identity.containerName], {
5866
+ timeoutMs: 3000
5867
+ });
5777
5868
  if (result.exitCode === 0 || containerDoesNotExist(result))
5778
5869
  this.registry.delete(identity.containerName, this.runtimeId);
5779
5870
  else
5780
5871
  this.registry.release(identity.containerName, this.runtimeId);
5781
- }));
5872
+ } catch {
5873
+ this.registry.release(identity.containerName, this.runtimeId);
5874
+ }
5782
5875
  }
5783
5876
  async suspendAll() {
5784
5877
  if (this.disposed)
5785
5878
  return;
5879
+ if (this.suspendPromise)
5880
+ return this.suspendPromise;
5881
+ this.suspending = true;
5882
+ this.suspendPromise = this.suspendUnlocked();
5883
+ return this.suspendPromise;
5884
+ }
5885
+ async suspendUnlocked() {
5886
+ await Promise.all([this.waitForOperations(), ...this.reconcilePromise ? [this.reconcilePromise.catch(() => {
5887
+ return;
5888
+ })] : []]);
5786
5889
  const identities = [...this.acquired.values()];
5787
5890
  await Promise.allSettled(identities.map(async (identity) => {
5788
5891
  if (!this.registry.ownedBy(identity.containerName, this.runtimeId))
5789
5892
  return;
5790
- const result = await this.runner("docker", ["stop", "-t", "1", identity.containerName]);
5791
- if (result.exitCode !== 0 && !containerDoesNotExist(result) && !containerAlreadyStopped(result))
5792
- return;
5793
- this.registry.release(identity.containerName, this.runtimeId);
5794
- this.acquired.delete(identity.containerName);
5893
+ try {
5894
+ const inspected = await this.inspectContainer(identity.containerName);
5895
+ if (inspected.error) {
5896
+ this.stopFailures.add(identity.containerName);
5897
+ return;
5898
+ }
5899
+ if (!inspected.exists || !managedContainerBelongsTo(inspected.labels, identity)) {
5900
+ this.registry.delete(identity.containerName, this.runtimeId);
5901
+ this.acquired.delete(identity.containerName);
5902
+ this.stopFailures.delete(identity.containerName);
5903
+ return;
5904
+ }
5905
+ const result = await this.runner("docker", ["stop", "-t", "1", identity.containerName], {
5906
+ timeoutMs: 1250
5907
+ });
5908
+ if (result.exitCode !== 0 && !containerDoesNotExist(result) && !containerAlreadyStopped(result)) {
5909
+ this.stopFailures.add(identity.containerName);
5910
+ return;
5911
+ }
5912
+ this.registry.release(identity.containerName, this.runtimeId);
5913
+ this.acquired.delete(identity.containerName);
5914
+ this.stopFailures.delete(identity.containerName);
5915
+ } catch {
5916
+ this.stopFailures.add(identity.containerName);
5917
+ }
5795
5918
  }));
5796
5919
  }
5797
5920
  async remove(identity) {
5798
5921
  this.assertOpen();
5799
- this.registry.claim(identity, this.runtimeId, CONTAINER_LEASE_MS);
5800
- this.acquired.set(identity.containerName, identity);
5801
- const result = await this.runner("docker", ["rm", "-f", "-v", identity.containerName]);
5802
- if (result.exitCode === 0 || containerDoesNotExist(result)) {
5803
- this.registry.delete(identity.containerName, this.runtimeId);
5804
- this.acquired.delete(identity.containerName);
5805
- if (result.exitCode !== 0)
5922
+ this.beginOperation();
5923
+ try {
5924
+ this.registry.claim(identity, this.runtimeId, CONTAINER_LEASE_MS);
5925
+ this.acquired.set(identity.containerName, identity);
5926
+ let inspected;
5927
+ try {
5928
+ inspected = await this.inspectContainer(identity.containerName);
5929
+ } catch (error) {
5930
+ this.stopFailures.add(identity.containerName);
5806
5931
  return {
5807
- ...result,
5808
- exitCode: 0,
5932
+ exitCode: 1,
5933
+ stdout: "",
5934
+ stderr: error instanceof Error ? error.message : String(error),
5935
+ durationMs: 0,
5936
+ timedOut: false
5937
+ };
5938
+ }
5939
+ if (inspected.error) {
5940
+ this.stopFailures.add(identity.containerName);
5941
+ return inspected.error;
5942
+ }
5943
+ if (inspected.exists && !managedContainerBelongsTo(inspected.labels, identity)) {
5944
+ this.registry.delete(identity.containerName, this.runtimeId);
5945
+ this.acquired.delete(identity.containerName);
5946
+ this.stopFailures.delete(identity.containerName);
5947
+ return {
5948
+ exitCode: 1,
5949
+ stdout: "",
5950
+ stderr: `refusing to remove container ${identity.containerName}: ownership labels do not match this Farai session`,
5951
+ durationMs: 0,
5809
5952
  timedOut: false
5810
5953
  };
5954
+ }
5955
+ const result = await this.runner("docker", ["rm", "-f", "-v", identity.containerName], {
5956
+ timeoutMs: 3000
5957
+ });
5958
+ if (result.exitCode === 0 || containerDoesNotExist(result)) {
5959
+ this.registry.delete(identity.containerName, this.runtimeId);
5960
+ this.acquired.delete(identity.containerName);
5961
+ this.stopFailures.delete(identity.containerName);
5962
+ if (result.exitCode !== 0)
5963
+ return {
5964
+ ...result,
5965
+ exitCode: 0,
5966
+ timedOut: false
5967
+ };
5968
+ } else {
5969
+ this.stopFailures.add(identity.containerName);
5970
+ }
5971
+ return result;
5972
+ } finally {
5973
+ this.endOperation();
5811
5974
  }
5812
- return result;
5813
5975
  }
5814
5976
  dispose() {
5977
+ if (this.disposePromise)
5978
+ return this.disposePromise;
5815
5979
  if (this.disposed)
5816
5980
  return;
5817
- for (const identity of this.acquired.values())
5818
- this.registry.release(identity.containerName, this.runtimeId);
5819
- this.acquired.clear();
5820
- this.registry.close();
5821
5981
  this.disposed = true;
5982
+ clearInterval(this.reconcileTimer);
5983
+ const finalize = () => {
5984
+ try {
5985
+ for (const identity of this.acquired.values()) {
5986
+ if (!this.stopFailures.has(identity.containerName))
5987
+ this.registry.release(identity.containerName, this.runtimeId);
5988
+ }
5989
+ } finally {
5990
+ this.acquired.clear();
5991
+ this.stopFailures.clear();
5992
+ this.registry.close();
5993
+ }
5994
+ };
5995
+ const waits = [...this.reconcilePromise ? [this.reconcilePromise.catch(() => {
5996
+ return;
5997
+ })] : [], ...this.suspendPromise ? [this.suspendPromise.catch(() => {
5998
+ return;
5999
+ })] : [], this.waitForOperations()];
6000
+ if (!this.reconcilePromise && !this.suspendPromise && this.activeOperations === 0) {
6001
+ finalize();
6002
+ return;
6003
+ }
6004
+ this.disposePromise = Promise.all(waits).then(finalize);
6005
+ return this.disposePromise;
6006
+ }
6007
+ beginOperation() {
6008
+ if (this.suspending)
6009
+ throw new Error("container lifecycle is closing");
6010
+ this.activeOperations += 1;
6011
+ }
6012
+ endOperation() {
6013
+ this.activeOperations -= 1;
6014
+ if (this.activeOperations === 0) {
6015
+ this.resolveOperationDrain?.();
6016
+ this.resolveOperationDrain = undefined;
6017
+ this.operationDrain = undefined;
6018
+ }
6019
+ }
6020
+ waitForOperations() {
6021
+ if (this.activeOperations === 0)
6022
+ return Promise.resolve();
6023
+ if (!this.operationDrain) {
6024
+ this.operationDrain = new Promise((resolve4) => {
6025
+ this.resolveOperationDrain = resolve4;
6026
+ });
6027
+ }
6028
+ return this.operationDrain;
6029
+ }
6030
+ async inspectContainer(containerName) {
6031
+ const result = await this.runner("docker", ["inspect", "-f", "{{json .Config.Labels}}", containerName]);
6032
+ if (result.exitCode !== 0) {
6033
+ if (containerDoesNotExist(result))
6034
+ return {
6035
+ exists: false
6036
+ };
6037
+ return {
6038
+ exists: true,
6039
+ error: result
6040
+ };
6041
+ }
6042
+ try {
6043
+ const labels = JSON.parse(result.stdout.trim());
6044
+ return {
6045
+ exists: true,
6046
+ ...labels ? {
6047
+ labels
6048
+ } : {}
6049
+ };
6050
+ } catch {
6051
+ return {
6052
+ exists: true
6053
+ };
6054
+ }
5822
6055
  }
5823
6056
  assertOpen() {
5824
6057
  if (this.disposed)
5825
6058
  throw new Error("container lifecycle is closed");
6059
+ if (this.suspending)
6060
+ throw new Error("container lifecycle is closing");
5826
6061
  }
5827
6062
  }
5828
6063
  function managedContainerLabels(identity) {
5829
6064
  return ["--label", `${FARAI_MANAGED_LABEL}=true`, "--label", `${FARAI_CONTAINER_KIND_LABEL}=${FARAI_INTERACTIVE_CONTAINER_KIND}`, "--label", `${FARAI_ROOT_SESSION_LABEL}=${identity.rootSessionId}`, "--label", `${FARAI_WORKSPACE_LABEL}=${encodeWorkspace(identity.workspace)}`, "--label", `${FARAI_WORKSPACE_HASH_LABEL}=${workspaceHash(identity.workspace)}`, "--label", `${FARAI_IMAGE_CONTRACT_LABEL}=${identity.imageContract}`];
5830
6065
  }
5831
6066
  function managedContainerLabelsMatch(labels, identity) {
5832
- return labels?.[FARAI_MANAGED_LABEL] === "true" && labels[FARAI_CONTAINER_KIND_LABEL] === FARAI_INTERACTIVE_CONTAINER_KIND && labels[FARAI_ROOT_SESSION_LABEL] === identity.rootSessionId && labels[FARAI_WORKSPACE_LABEL] === encodeWorkspace(identity.workspace) && labels[FARAI_WORKSPACE_HASH_LABEL] === workspaceHash(identity.workspace) && labels[FARAI_IMAGE_CONTRACT_LABEL] === identity.imageContract;
6067
+ return managedContainerBelongsTo(labels, identity) && labels?.[FARAI_IMAGE_CONTRACT_LABEL] === identity.imageContract;
6068
+ }
6069
+ function managedContainerBelongsTo(labels, identity) {
6070
+ return labels?.[FARAI_MANAGED_LABEL] === "true" && labels[FARAI_CONTAINER_KIND_LABEL] === FARAI_INTERACTIVE_CONTAINER_KIND && labels[FARAI_ROOT_SESSION_LABEL] === identity.rootSessionId && labels[FARAI_WORKSPACE_LABEL] === encodeWorkspace(identity.workspace) && (!labels[FARAI_WORKSPACE_HASH_LABEL] || labels[FARAI_WORKSPACE_HASH_LABEL] === workspaceHash(identity.workspace));
5833
6071
  }
5834
6072
  function workspaceHash(workspace) {
5835
6073
  return createHash3("sha256").update(resolve3(workspace)).digest("hex");
@@ -5906,22 +6144,34 @@ class ContainerLeaseRegistry {
5906
6144
  $now: now
5907
6145
  });
5908
6146
  }
5909
- expireLeases() {
6147
+ expireLeases(runtimeId, leaseMs) {
5910
6148
  const now = new Date().toISOString();
5911
6149
  const rows = this.db.query(`select * from container_leases
5912
- where lease_owner is not null and lease_expires_at is not null and lease_expires_at <= $now`).all({
5913
- $now: now
6150
+ where lease_owner is not null and lease_owner != $owner and lease_expires_at is not null and lease_expires_at <= $now`).all({
6151
+ $now: now,
6152
+ $owner: runtimeId
5914
6153
  });
5915
- const update = this.db.query(`update container_leases set lease_owner = null, lease_expires_at = null,
5916
- idle_since = coalesce(idle_since, $now), last_used_at = $now where container_name = $name`);
6154
+ const expires = new Date(Date.now() + leaseMs).toISOString();
6155
+ const update = this.db.query(`update container_leases set lease_owner = $owner, lease_expires_at = $expires,
6156
+ idle_since = coalesce(idle_since, $idle), last_used_at = $now
6157
+ where container_name = $name and lease_owner = $previousOwner and lease_expires_at = $previousExpires`);
6158
+ const claimed = [];
5917
6159
  this.db.transaction(() => {
5918
- for (const row of rows)
5919
- update.run({
6160
+ for (const row of rows) {
6161
+ const result = update.run({
5920
6162
  $name: row.container_name,
5921
- $now: row.lease_expires_at ?? now
6163
+ $previousOwner: row.lease_owner,
6164
+ $previousExpires: row.lease_expires_at,
6165
+ $owner: runtimeId,
6166
+ $expires: expires,
6167
+ $idle: row.lease_expires_at ?? now,
6168
+ $now: now
5922
6169
  });
5923
- })();
5924
- return rows.map(identityFromRow);
6170
+ if (result.changes === 1)
6171
+ claimed.push(row);
6172
+ }
6173
+ }).immediate();
6174
+ return claimed.map(identityFromRow);
5925
6175
  }
5926
6176
  claimExpiredIdle(runtimeId, idleTtlMs) {
5927
6177
  const cutoff = new Date(Date.now() - Math.max(0, idleTtlMs)).toISOString();
@@ -5931,14 +6181,16 @@ class ContainerLeaseRegistry {
5931
6181
  });
5932
6182
  const expires = new Date(Date.now() + CONTAINER_LEASE_MS).toISOString();
5933
6183
  const claim = this.db.query(`update container_leases set lease_owner = $owner, lease_expires_at = $expires
5934
- where container_name = $name and lease_owner is null`);
6184
+ where container_name = $name and lease_owner is null and lease_expires_at is null
6185
+ and idle_since = $idle`);
5935
6186
  const claimed = [];
5936
6187
  this.db.transaction(() => {
5937
6188
  for (const row of rows) {
5938
6189
  const result = claim.run({
5939
6190
  $name: row.container_name,
5940
6191
  $owner: runtimeId,
5941
- $expires: expires
6192
+ $expires: expires,
6193
+ $idle: row.idle_since
5942
6194
  });
5943
6195
  if (result.changes === 1)
5944
6196
  claimed.push(row);
@@ -5948,9 +6200,22 @@ class ContainerLeaseRegistry {
5948
6200
  }
5949
6201
  adopt(identity) {
5950
6202
  const now = new Date().toISOString();
5951
- this.db.query(`insert or ignore into container_leases
6203
+ this.db.query(`insert into container_leases
5952
6204
  (container_name, root_session_id, workspace, image_contract, lease_owner, lease_expires_at, idle_since, last_used_at)
5953
- values ($name, $root, $workspace, $contract, null, null, $now, $now)`).run({
6205
+ values ($name, $root, $workspace, $contract, null, null, $now, $now)
6206
+ on conflict(container_name) do update set
6207
+ root_session_id = excluded.root_session_id,
6208
+ workspace = excluded.workspace,
6209
+ image_contract = excluded.image_contract,
6210
+ idle_since = case when container_leases.root_session_id <> excluded.root_session_id
6211
+ or container_leases.workspace <> excluded.workspace
6212
+ or container_leases.image_contract <> excluded.image_contract
6213
+ then excluded.idle_since else coalesce(container_leases.idle_since, excluded.idle_since) end,
6214
+ last_used_at = case when container_leases.root_session_id <> excluded.root_session_id
6215
+ or container_leases.workspace <> excluded.workspace
6216
+ or container_leases.image_contract <> excluded.image_contract
6217
+ then excluded.last_used_at else container_leases.last_used_at end
6218
+ where container_leases.lease_owner is null`).run({
5954
6219
  $name: identity.containerName,
5955
6220
  $root: identity.rootSessionId,
5956
6221
  $workspace: resolve3(identity.workspace),
@@ -5958,6 +6223,20 @@ class ContainerLeaseRegistry {
5958
6223
  $now: now
5959
6224
  });
5960
6225
  }
6226
+ claimUnleased(identity, runtimeId, leaseMs) {
6227
+ const expires = new Date(Date.now() + leaseMs).toISOString();
6228
+ const result = this.db.query(`update container_leases set lease_owner = $owner, lease_expires_at = $expires
6229
+ where container_name = $name and root_session_id = $root and workspace = $workspace
6230
+ and image_contract = $contract and lease_owner is null and lease_expires_at is null`).run({
6231
+ $name: identity.containerName,
6232
+ $root: identity.rootSessionId,
6233
+ $workspace: resolve3(identity.workspace),
6234
+ $contract: identity.imageContract,
6235
+ $owner: runtimeId,
6236
+ $expires: expires
6237
+ });
6238
+ return result.changes === 1;
6239
+ }
5961
6240
  has(containerName) {
5962
6241
  return Boolean(this.row(containerName));
5963
6242
  }
@@ -6012,15 +6291,22 @@ class ContainerLeaseRegistry {
6012
6291
  }
6013
6292
  }
6014
6293
  async function listManagedContainers(runner) {
6015
- const format = `{{.Names}} {{.State}} {{.Label "${FARAI_ROOT_SESSION_LABEL}"}} {{.Label "${FARAI_WORKSPACE_LABEL}"}} {{.Label "${FARAI_IMAGE_CONTRACT_LABEL}"}}`;
6016
- const result = await runner("docker", ["ps", "-a", "--filter", `label=${FARAI_MANAGED_LABEL}=true`, "--filter", `label=${FARAI_CONTAINER_KIND_LABEL}=${FARAI_INTERACTIVE_CONTAINER_KIND}`, "--format", format]);
6294
+ const format = `{{.Names}} {{.State}} {{.Label "${FARAI_ROOT_SESSION_LABEL}"}} {{.Label "${FARAI_WORKSPACE_LABEL}"}} {{.Label "${FARAI_WORKSPACE_HASH_LABEL}"}} {{.Label "${FARAI_IMAGE_CONTRACT_LABEL}"}}`;
6295
+ const result = await runner("docker", ["ps", "-a", "--filter", `label=${FARAI_MANAGED_LABEL}=true`, "--filter", `label=${FARAI_CONTAINER_KIND_LABEL}=${FARAI_INTERACTIVE_CONTAINER_KIND}`, "--format", format], {
6296
+ timeoutMs: 3000
6297
+ });
6017
6298
  if (result.exitCode !== 0)
6018
6299
  return;
6019
6300
  return result.stdout.split(`
6020
6301
  `).flatMap((line) => {
6021
- const [containerName, state, rootSessionId, encodedWorkspace, imageContract] = line.trim().split("\t");
6302
+ const fields = line.trim().split("\t");
6303
+ const [containerName, state, rootSessionId, encodedWorkspace] = fields;
6304
+ const encodedWorkspaceHash = fields.length >= 6 ? fields[4] : "";
6305
+ const imageContract = fields.length >= 6 ? fields[5] : fields[4];
6022
6306
  const workspace = encodedWorkspace ? decodeWorkspace(encodedWorkspace) : undefined;
6023
- if (!containerName || !state || !rootSessionId || !workspace || !imageContract)
6307
+ if (!containerName || !containerName.startsWith(FARAI_CONTAINER_NAME_PREFIX) || !state || !rootSessionId || !workspace || !imageContract)
6308
+ return [];
6309
+ if (encodedWorkspaceHash && encodedWorkspaceHash !== workspaceHash(workspace))
6024
6310
  return [];
6025
6311
  return [{
6026
6312
  containerName,
@@ -6040,20 +6326,29 @@ function identityFromRow(row) {
6040
6326
  };
6041
6327
  }
6042
6328
  function containerDoesNotExist(result) {
6043
- return /no such container/i.test(`${result.stdout}
6329
+ return /no such (container|object)/i.test(`${result.stdout}
6044
6330
  ${result.stderr}`);
6045
6331
  }
6046
6332
  function containerAlreadyStopped(result) {
6047
6333
  return /is not running|already stopped/i.test(`${result.stdout}
6048
6334
  ${result.stderr}`);
6049
6335
  }
6050
- async function runDockerProcess(command, args, timeoutMs = 15000) {
6336
+ function containerNeedsStop(state) {
6337
+ return ["created", "running", "restarting", "paused"].includes(state.toLowerCase());
6338
+ }
6339
+ function managedContainerIdentityMatches(left, right) {
6340
+ return left.rootSessionId === right.rootSessionId && resolve3(left.workspace) === resolve3(right.workspace);
6341
+ }
6342
+ async function runDockerProcess(command, args, options = {}) {
6051
6343
  return await runCapturedProcess(command, args, {
6052
- timeoutMs,
6344
+ timeoutMs: options.timeoutMs ?? 3000,
6345
+ ...options.signal ? {
6346
+ signal: options.signal
6347
+ } : {},
6053
6348
  env: faraiDockerEnvironment()
6054
6349
  });
6055
6350
  }
6056
- var FARAI_MANAGED_LABEL = "org.farai.managed", FARAI_CONTAINER_KIND_LABEL = "org.farai.kind", FARAI_ROOT_SESSION_LABEL = "org.farai.root-session", FARAI_WORKSPACE_LABEL = "org.farai.workspace", FARAI_WORKSPACE_HASH_LABEL = "org.farai.workspace-hash", FARAI_IMAGE_CONTRACT_LABEL = "org.farai.image-contract", FARAI_INTERACTIVE_CONTAINER_KIND = "interactive", CONTAINER_LEASE_MS = 60000, CONTAINER_IDLE_TTL_MS;
6351
+ var FARAI_MANAGED_LABEL = "org.farai.managed", FARAI_CONTAINER_KIND_LABEL = "org.farai.kind", FARAI_ROOT_SESSION_LABEL = "org.farai.root-session", FARAI_WORKSPACE_LABEL = "org.farai.workspace", FARAI_WORKSPACE_HASH_LABEL = "org.farai.workspace-hash", FARAI_IMAGE_CONTRACT_LABEL = "org.farai.image-contract", FARAI_INTERACTIVE_CONTAINER_KIND = "interactive", FARAI_CONTAINER_NAME_PREFIX = "farai-kali-", CONTAINER_LEASE_MS = 60000, CONTAINER_IDLE_TTL_MS, CONTAINER_RECONCILE_INTERVAL_MS = 60000;
6057
6352
  var init_lifecycle = __esm(() => {
6058
6353
  init_config();
6059
6354
  init_captured_process();
@@ -6121,12 +6416,15 @@ class KaliContainerBackend {
6121
6416
  async status() {
6122
6417
  const image = await this.resolveImage();
6123
6418
  const dockerContext = (await this.processRunner("docker", ["context", "show"])).stdout.trim();
6124
- const inspected = await this.inspectPersistent();
6419
+ const inspectedResult = await this.inspectPersistent();
6420
+ const inspected = inspectedResult.container;
6125
6421
  const persistentExists = Boolean(inspected);
6126
6422
  const persistentRunning = inspected?.state === "running";
6127
6423
  const persistentImageId = inspected?.imageId ?? "";
6128
6424
  const persistentImageCurrent = Boolean(persistentExists && image.id && persistentImageId && imageIdsMatch(image.id, persistentImageId));
6129
6425
  const persistentIdentityCurrent = Boolean(this.identity && managedContainerLabelsMatch(inspected?.labels, this.identity));
6426
+ const persistentManaged = Boolean(this.identity && managedContainerBelongsTo(inspected?.labels, this.identity));
6427
+ const dockerError = image.error ?? inspectedResult.error;
6130
6428
  return {
6131
6429
  image: this.image,
6132
6430
  imageExists: image.exists,
@@ -6150,30 +6448,43 @@ class KaliContainerBackend {
6150
6448
  persistentImageId
6151
6449
  } : {},
6152
6450
  persistentImageCurrent,
6153
- persistentIdentityCurrent
6451
+ persistentIdentityCurrent,
6452
+ persistentManaged,
6453
+ ...dockerError ? {
6454
+ dockerError
6455
+ } : {}
6154
6456
  };
6155
6457
  }
6156
6458
  async inspectPersistent() {
6157
6459
  const inspected = await this.processRunner("docker", ["inspect", "-f", "{{.State.Status}}\t{{.Image}}\t{{json .Config.Labels}}", this.containerName]);
6158
- if (inspected.exitCode !== 0)
6159
- return;
6460
+ if (inspected.exitCode !== 0) {
6461
+ if (containerDoesNotExist2(inspected) || !inspected.stdout.trim() && !inspected.stderr.trim())
6462
+ return {};
6463
+ return {
6464
+ error: dockerFailure(inspected, `unable to inspect persistent Kali container ${this.containerName}`)
6465
+ };
6466
+ }
6160
6467
  const [state = "unknown", imageId = "", labelsJson = ""] = inspected.stdout.trim().split("\t", 3);
6161
6468
  try {
6162
6469
  return {
6163
- state: state || "unknown",
6164
- ...imageId ? {
6165
- imageId
6166
- } : {},
6167
- ...labelsJson && labelsJson !== "null" ? {
6168
- labels: JSON.parse(labelsJson)
6169
- } : {}
6470
+ container: {
6471
+ state: state || "unknown",
6472
+ ...imageId ? {
6473
+ imageId
6474
+ } : {},
6475
+ ...labelsJson && labelsJson !== "null" ? {
6476
+ labels: JSON.parse(labelsJson)
6477
+ } : {}
6478
+ }
6170
6479
  };
6171
6480
  } catch {
6172
6481
  return {
6173
- state: state || "unknown",
6174
- ...imageId ? {
6175
- imageId
6176
- } : {}
6482
+ container: {
6483
+ state: state || "unknown",
6484
+ ...imageId ? {
6485
+ imageId
6486
+ } : {}
6487
+ }
6177
6488
  };
6178
6489
  }
6179
6490
  }
@@ -6191,6 +6502,12 @@ class KaliContainerBackend {
6191
6502
  exists: false
6192
6503
  };
6193
6504
  const listed = await this.processRunner("docker", ["image", "ls", repository, "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"]);
6505
+ if (listed.exitCode !== 0) {
6506
+ return {
6507
+ exists: false,
6508
+ error: dockerFailure(listed, `unable to inspect local Docker images for ${this.image}`)
6509
+ };
6510
+ }
6194
6511
  const line = listed.stdout.split(`
6195
6512
  `).map((candidate) => candidate.trim()).find((candidate) => candidate.startsWith(`${repository}:${tag} `));
6196
6513
  if (!line)
@@ -6206,7 +6523,8 @@ class KaliContainerBackend {
6206
6523
  if (inspectById.exitCode !== 0)
6207
6524
  return {
6208
6525
  exists: true,
6209
- id: id2
6526
+ id: id2,
6527
+ error: dockerFailure(inspectById, `unable to inspect local Docker image ${id2}`)
6210
6528
  };
6211
6529
  const resolved = parseImageInspect(inspectById.stdout);
6212
6530
  return {
@@ -6229,8 +6547,14 @@ class KaliContainerBackend {
6229
6547
  }
6230
6548
  }
6231
6549
  async startPersistentUnlocked() {
6550
+ if (this.identity && this.lifecycle?.withLease) {
6551
+ return await this.lifecycle.withLease(this.identity, () => this.startPersistentBody());
6552
+ }
6232
6553
  if (this.identity && this.lifecycle)
6233
6554
  await this.lifecycle.acquire(this.identity);
6555
+ return await this.startPersistentBody();
6556
+ }
6557
+ async startPersistentBody() {
6234
6558
  try {
6235
6559
  const status = await this.status();
6236
6560
  if (!status.imageExists) {
@@ -6239,7 +6563,28 @@ class KaliContainerBackend {
6239
6563
  return {
6240
6564
  exitCode: 1,
6241
6565
  stdout: "",
6242
- stderr: `kali image ${this.image} is missing; run \`farai setup --no-kb\``,
6566
+ stderr: status.dockerError ?? `kali image ${this.image} is missing; run \`farai setup --no-kb\``,
6567
+ durationMs: 0,
6568
+ timedOut: false
6569
+ };
6570
+ }
6571
+ if (status.dockerError) {
6572
+ if (this.identity && this.lifecycle)
6573
+ this.lifecycle.release(this.identity);
6574
+ return {
6575
+ exitCode: 1,
6576
+ stdout: "",
6577
+ stderr: status.dockerError,
6578
+ durationMs: 0,
6579
+ timedOut: false
6580
+ };
6581
+ }
6582
+ if (status.persistentExists && this.identity && !status.persistentManaged) {
6583
+ this.lifecycle?.release(this.identity);
6584
+ return {
6585
+ exitCode: 1,
6586
+ stdout: "",
6587
+ stderr: `refusing to use container ${this.containerName}: ownership labels do not match this Farai session`,
6243
6588
  durationMs: 0,
6244
6589
  timedOut: false
6245
6590
  };
@@ -6276,7 +6621,15 @@ class KaliContainerBackend {
6276
6621
  };
6277
6622
  }
6278
6623
  return await withGlobalContainerStartLock(async () => {
6279
- await this.processRunner("docker", ["rm", "-f", "-v", this.containerName]);
6624
+ const removed = await this.processRunner("docker", ["rm", "-f", "-v", this.containerName]);
6625
+ if (removed.exitCode !== 0 && !containerDoesNotExist2(removed)) {
6626
+ if (this.identity && this.lifecycle)
6627
+ this.lifecycle.release(this.identity);
6628
+ return {
6629
+ ...removed,
6630
+ stderr: removed.stderr || `could not remove stale Kali container ${this.containerName}`
6631
+ };
6632
+ }
6280
6633
  const worktrees = join9(this.rootWorkspace, ".farai", "worktrees");
6281
6634
  mkdirSync4(worktrees, {
6282
6635
  recursive: true
@@ -6318,6 +6671,11 @@ class KaliContainerBackend {
6318
6671
  };
6319
6672
  }
6320
6673
  async stopPersistent() {
6674
+ const previous = containerStartLocks.get(this.containerName);
6675
+ if (previous)
6676
+ await previous.catch(() => {
6677
+ return;
6678
+ });
6321
6679
  if (this.identity && this.lifecycle)
6322
6680
  return await this.lifecycle.remove(this.identity);
6323
6681
  const started = Date.now();
@@ -6676,6 +7034,11 @@ function parseImageInspect(raw) {
6676
7034
  };
6677
7035
  }
6678
7036
  }
7037
+ function dockerFailure(result, fallback) {
7038
+ const detail = `${result.stderr}
7039
+ ${result.stdout}`.trim().replace(/\s+/g, " ");
7040
+ return detail ? `${fallback}: ${detail.slice(0, 500)}` : fallback;
7041
+ }
6679
7042
  function imageIdsMatch(left, right) {
6680
7043
  const normalizedLeft = left.replace(/^sha256:/, "");
6681
7044
  const normalizedRight = right.replace(/^sha256:/, "");
@@ -6691,10 +7054,10 @@ async function runProcess(command, args, timeoutMs = 15000) {
6691
7054
  });
6692
7055
  }
6693
7056
  function containerDoesNotExist2(result) {
6694
- return /no such container/i.test(`${result.stdout}
7057
+ return /no such (container|object)/i.test(`${result.stdout}
6695
7058
  ${result.stderr}`);
6696
7059
  }
6697
- var CONTAINER_WORKSPACE_MOUNT = "/workspace", CONTAINER_WORKTREES_MOUNT = "/worktrees", kaliSessions, kaliPtySessions, containerStartLocks, globalContainerStartChain, CONTAINER_EXEC_MARKER_DIR = "/tmp/farai-exec", CONTAINER_EXEC_WRAPPER, CONTAINER_EXEC_KILLER, CONTAINER_PREFIX = "farai-kali-", KALI_IMAGE_CONTRACT, DEFAULT_KALI_IMAGE = "farai-kali:latest", KALI_IMAGE_CONTRACT_LABEL = "org.farai.kali.contract";
7060
+ var CONTAINER_WORKSPACE_MOUNT = "/workspace", CONTAINER_WORKTREES_MOUNT = "/worktrees", kaliSessions, kaliPtySessions, containerStartLocks, globalContainerStartChain, CONTAINER_EXEC_MARKER_DIR = "/tmp/farai-exec", CONTAINER_EXEC_WRAPPER, CONTAINER_EXEC_KILLER, CONTAINER_PREFIX, KALI_IMAGE_CONTRACT, DEFAULT_KALI_IMAGE = "farai-kali:latest", KALI_IMAGE_CONTRACT_LABEL = "org.farai.kali.contract";
6698
7061
  var init_kali = __esm(() => {
6699
7062
  init_spawn_session();
6700
7063
  init_pty_session();
@@ -6711,6 +7074,7 @@ var init_kali = __esm(() => {
6711
7074
  `);
6712
7075
  CONTAINER_EXEC_KILLER = ["import os, signal, sys, time", "path = sys.argv[1]", "root = None", "marker_deadline = time.monotonic() + 0.25", "while root is None and time.monotonic() < marker_deadline:", " try:", " with open(path, encoding='ascii') as handle:", " root = int(handle.read().strip())", " except (FileNotFoundError, OSError, ValueError):", " time.sleep(0.01)", "if root is None:", " raise SystemExit(0)", "def direct_children(pid):", " try:", " with open(f'/proc/{pid}/task/{pid}/children', encoding='ascii') as handle:", " return [int(value) for value in handle.read().split()]", " except (FileNotFoundError, OSError, ValueError):", " return []", "def descendants(pid):", " found = []", " pending = direct_children(pid)", " seen = set()", " while pending:", " child = pending.pop()", " if child in seen:", " continue", " seen.add(child)", " found.append(child)", " pending.extend(direct_children(child))", " return found", "def alive(pid):", " try:", " os.kill(pid, 0)", " return True", " except ProcessLookupError:", " return False", " except PermissionError:", " return True", "targets = descendants(root)", "for pid in [*reversed(targets), root]:", " try:", " os.kill(pid, signal.SIGTERM)", " except (ProcessLookupError, PermissionError):", " pass", "deadline = time.monotonic() + 1.0", "while time.monotonic() < deadline and any(alive(pid) for pid in [root, *targets]):", " time.sleep(0.025)", "targets = list(dict.fromkeys([*targets, *descendants(root)]))", "for pid in [*reversed(targets), root]:", " try:", " os.kill(pid, signal.SIGKILL)", " except (ProcessLookupError, PermissionError):", " pass", "try:", " os.unlink(path)", "except FileNotFoundError:", " pass"].join(`
6713
7076
  `);
7077
+ CONTAINER_PREFIX = FARAI_CONTAINER_NAME_PREFIX;
6714
7078
  KALI_IMAGE_CONTRACT = KALI_TOOL_MANIFEST.contract;
6715
7079
  });
6716
7080
 
@@ -7463,8 +7827,8 @@ var init_process_output = __esm(() => {
7463
7827
 
7464
7828
  // src/version.ts
7465
7829
  function resolveFaraiVersion() {
7466
- if ("0.2.8")
7467
- return "0.2.8";
7830
+ if ("0.2.9")
7831
+ return "0.2.9";
7468
7832
  try {
7469
7833
  const parsed = JSON.parse(readBoundedFileTextSync(new URL("../package.json", import.meta.url), 1024 * 1024, "package metadata"));
7470
7834
  if (typeof parsed.version === "string" && parsed.version)
@@ -16905,7 +17269,7 @@ var init_patch_apply = __esm(() => {
16905
17269
  });
16906
17270
 
16907
17271
  // src/agent-tools/filesystem/notebook-edit.ts
16908
- import { statSync as statSync2 } from "fs";
17272
+ import { statSync as statSync3 } from "fs";
16909
17273
  import { randomUUID as randomUUID2 } from "crypto";
16910
17274
  function parseNotebook(text2) {
16911
17275
  const value = JSON.parse(text2);
@@ -16922,7 +17286,7 @@ function parseNotebook(text2) {
16922
17286
  return value;
16923
17287
  }
16924
17288
  function writeNotebookAtomically(path, content) {
16925
- const mode = statSync2(path).mode & 511;
17289
+ const mode = statSync3(path).mode & 511;
16926
17290
  atomicWriteFile(path, content, mode);
16927
17291
  }
16928
17292
  function newCell(cellType, source, includeId) {
@@ -17408,6 +17772,9 @@ function parseActiveContent(value) {
17408
17772
  return;
17409
17773
  if (!validDate(record2.generatedAt) || !validDate(record2.activatedAt))
17410
17774
  return;
17775
+ if (record2.sourceCommit !== undefined && (typeof record2.sourceCommit !== "string" || !SOURCE_COMMIT_PATTERN.test(record2.sourceCommit)))
17776
+ return;
17777
+ const sourceCommit = typeof record2.sourceCommit === "string" ? record2.sourceCommit : undefined;
17411
17778
  if (typeof record2.manifestUrl !== "string" || !record2.manifestUrl)
17412
17779
  return;
17413
17780
  if (typeof record2.knowledge !== "boolean" || typeof record2.skills !== "boolean")
@@ -17417,6 +17784,9 @@ function parseActiveContent(value) {
17417
17784
  schemaVersion: 1,
17418
17785
  version: record2.version,
17419
17786
  generatedAt: record2.generatedAt,
17787
+ ...sourceCommit ? {
17788
+ sourceCommit
17789
+ } : {},
17420
17790
  activatedAt: record2.activatedAt,
17421
17791
  manifestUrl: record2.manifestUrl,
17422
17792
  ...previousVersion ? {
@@ -17429,18 +17799,19 @@ function parseActiveContent(value) {
17429
17799
  function validDate(value) {
17430
17800
  return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value));
17431
17801
  }
17432
- var POINTER_MAX_BYTES, VERSION_PATTERN;
17802
+ var POINTER_MAX_BYTES, VERSION_PATTERN, SOURCE_COMMIT_PATTERN;
17433
17803
  var init_paths2 = __esm(() => {
17434
17804
  init_paths();
17435
17805
  init_private_path();
17436
17806
  init_file_read();
17437
17807
  POINTER_MAX_BYTES = 64 * 1024;
17438
17808
  VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17809
+ SOURCE_COMMIT_PATTERN = /^[a-f0-9]{40}$/;
17439
17810
  });
17440
17811
 
17441
17812
  // src/agent-skills/registry.ts
17442
17813
  import { createHash as createHash5 } from "crypto";
17443
- import { existsSync as existsSync9, readdirSync as readdirSync3, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
17814
+ import { existsSync as existsSync9, readdirSync as readdirSync3, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
17444
17815
  import { homedir as homedir4 } from "os";
17445
17816
  import { delimiter, isAbsolute as isAbsolute5, join as join14, relative as relative4, resolve as resolve8, sep as sep2 } from "path";
17446
17817
  function discoverSkills(options = {}) {
@@ -17501,7 +17872,7 @@ function loadSkill(name, options = {}) {
17501
17872
  const canonical = realpathSync2(absolute);
17502
17873
  if (!inside(skill.directory, canonical))
17503
17874
  return;
17504
- const stats = statSync3(canonical);
17875
+ const stats = statSync4(canonical);
17505
17876
  if (!stats.isFile() || stats.size > MAX_RESOURCE_BYTES)
17506
17877
  return;
17507
17878
  const content = readBoundedFileTextSync(canonical, MAX_RESOURCE_BYTES, "skill resource");
@@ -17533,11 +17904,7 @@ function renderSkillCatalog(workspace, maxChars = 8000) {
17533
17904
  `), maxChars);
17534
17905
  }
17535
17906
  function skillRoots(options) {
17536
- const roots = [{
17537
- path: BUILTIN_DIR,
17538
- source: "builtin",
17539
- priority: 0
17540
- }];
17907
+ const roots = [];
17541
17908
  const content = activeContentSkillsDir();
17542
17909
  if (content)
17543
17910
  roots.push({
@@ -17604,10 +17971,6 @@ function scanRoot(root, diagnostics) {
17604
17971
  }
17605
17972
  return skills;
17606
17973
  }
17607
- function resolveBuiltinSkillDir() {
17608
- const candidates = [join14(import.meta.dir, "library"), join14(import.meta.dir, "..", "..", "src", "agent-skills", "library")];
17609
- return candidates.find((candidate) => existsSync9(candidate)) ?? candidates[0];
17610
- }
17611
17974
  function parseSkill(file, directory, directoryName, root, diagnostics) {
17612
17975
  let raw;
17613
17976
  try {
@@ -17812,11 +18175,10 @@ function compactText(value, max) {
17812
18175
  function errorMessage(error) {
17813
18176
  return error instanceof Error ? error.message : String(error);
17814
18177
  }
17815
- var BUILTIN_DIR, MAX_SKILL_BYTES, RECOMMENDED_SKILL_BYTES, MAX_RESOURCE_BYTES, MAX_RESOURCES = 256, NAME_PATTERN;
18178
+ var MAX_SKILL_BYTES, RECOMMENDED_SKILL_BYTES, MAX_RESOURCE_BYTES, MAX_RESOURCES = 256, NAME_PATTERN;
17816
18179
  var init_registry2 = __esm(() => {
17817
18180
  init_file_read();
17818
18181
  init_paths2();
17819
- BUILTIN_DIR = resolveBuiltinSkillDir();
17820
18182
  MAX_SKILL_BYTES = 64 * 1024;
17821
18183
  RECOMMENDED_SKILL_BYTES = 20 * 1024;
17822
18184
  MAX_RESOURCE_BYTES = 128 * 1024;
@@ -17951,7 +18313,7 @@ var init_global_config = __esm(() => {
17951
18313
 
17952
18314
  // src/agent-core/context-builder.ts
17953
18315
  import { execFileSync } from "child_process";
17954
- import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
18316
+ import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
17955
18317
  import { isAbsolute as isAbsolute6, join as join15, normalize as normalize2, relative as relative5 } from "path";
17956
18318
 
17957
18319
  class ContextBuilderCache {
@@ -18205,7 +18567,7 @@ function workspaceFileFingerprint(workspace) {
18205
18567
  }
18206
18568
  function statFingerprint(path) {
18207
18569
  try {
18208
- const stat = statSync4(path);
18570
+ const stat = statSync5(path);
18209
18571
  return `${path}:${stat.mtimeMs}:${stat.size}:${stat.ino}`;
18210
18572
  } catch {
18211
18573
  return `${path}:missing`;
@@ -18219,7 +18581,7 @@ function validWorkspaceFiles(workspace, paths) {
18219
18581
  continue;
18220
18582
  const absolute = join15(workspace, rel);
18221
18583
  try {
18222
- if (statSync4(absolute).isFile())
18584
+ if (statSync5(absolute).isFile())
18223
18585
  files.push(rel);
18224
18586
  } catch {}
18225
18587
  }
@@ -22905,7 +23267,7 @@ var init_web = __esm(() => {
22905
23267
  });
22906
23268
 
22907
23269
  // src/agent-tools/media/image-view.ts
22908
- import { statSync as statSync5 } from "fs";
23270
+ import { statSync as statSync6 } from "fs";
22909
23271
  import { basename as basename3, relative as relative6 } from "path";
22910
23272
  function detectImage(data) {
22911
23273
  if (data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
@@ -22954,7 +23316,7 @@ var init_image_view = __esm(() => {
22954
23316
  assertObject(args, "args");
22955
23317
  const requested = asString(args.path, "path");
22956
23318
  const path = safeExistingWorkspacePath(context.workspace, requested, "read");
22957
- const stat = statSync5(path);
23319
+ const stat = statSync6(path);
22958
23320
  if (!stat.isFile())
22959
23321
  throw new Error(`not a file: ${requested}`);
22960
23322
  if (stat.size > MAX_IMAGE_BYTES)
@@ -34806,6 +35168,7 @@ class AgentRuntime {
34806
35168
  for (const controller of this.subagentControllers.values())
34807
35169
  controller.abort("runtime shutdown");
34808
35170
  const drain = Promise.allSettled([...[...this.actors.values()].map((actor) => actor.idle()), this.workspaceBindingGate.idle(), this.toolExecutionGate.idle(), this.subagentGate.idle(), this.subagentWorkspaceMutationGate.idle(), ...this.recoveryPromise ? [this.recoveryPromise] : []]);
35171
+ const drainGracePeriod = shutdownGracePeriod(options.gracePeriodMs);
34809
35172
  this.shutdownFinalizationPromise = (async () => {
34810
35173
  const failures = [];
34811
35174
  const attempt = async (label, operation) => {
@@ -34818,7 +35181,11 @@ class AgentRuntime {
34818
35181
  }
34819
35182
  };
34820
35183
  try {
34821
- await drain;
35184
+ const drained = await waitForShutdownFinalization(drain, drainGracePeriod);
35185
+ if (!drained) {
35186
+ for (const lease of this.activeToolLeases)
35187
+ lease.revoke("runtime shutdown grace period expired");
35188
+ }
34822
35189
  this.activeToolControllers.clear();
34823
35190
  this.activeToolLeases.clear();
34824
35191
  this.subagentControllers.clear();
@@ -34995,6 +35362,10 @@ class AgentRuntime {
34995
35362
  await this.recoveryPromise;
34996
35363
  } catch (error) {
34997
35364
  this.stopRuntimeLease();
35365
+ if (this.shuttingDown)
35366
+ throw new Error("Farai runtime is shutting down", {
35367
+ cause: error
35368
+ });
34998
35369
  throw error;
34999
35370
  } finally {
35000
35371
  this.recoveryPromise = undefined;
@@ -39335,6 +39706,7 @@ function parseContentManifest(value, baseUrl) {
39335
39706
  throw new Error("invalid content version");
39336
39707
  if (typeof record3.generatedAt !== "string" || !Number.isFinite(Date.parse(record3.generatedAt)))
39337
39708
  throw new Error("invalid content generatedAt");
39709
+ const sourceCommit = optionalSourceCommit(record3.sourceCommit);
39338
39710
  const minFaraiVersion = optionalString2(record3.minFaraiVersion, 128, "minFaraiVersion");
39339
39711
  if (minFaraiVersion && !isSemver(minFaraiVersion))
39340
39712
  throw new Error("invalid content minFaraiVersion");
@@ -39345,6 +39717,9 @@ function parseContentManifest(value, baseUrl) {
39345
39717
  schemaVersion: 1,
39346
39718
  contentVersion: record3.contentVersion,
39347
39719
  generatedAt: new Date(record3.generatedAt).toISOString(),
39720
+ ...sourceCommit ? {
39721
+ sourceCommit
39722
+ } : {},
39348
39723
  ...minFaraiVersion ? {
39349
39724
  minFaraiVersion
39350
39725
  } : {},
@@ -39359,6 +39734,13 @@ function parseContentManifest(value, baseUrl) {
39359
39734
  } : {}
39360
39735
  };
39361
39736
  }
39737
+ function optionalSourceCommit(value) {
39738
+ if (value === undefined)
39739
+ return;
39740
+ if (typeof value !== "string" || !SOURCE_COMMIT_PATTERN2.test(value))
39741
+ throw new Error("invalid content sourceCommit");
39742
+ return value;
39743
+ }
39362
39744
  function artifact(value, baseUrl, label) {
39363
39745
  if (value === undefined)
39364
39746
  return;
@@ -39395,13 +39777,14 @@ function optionalString2(value, maxLength, label) {
39395
39777
  throw new Error(`invalid content ${label}`);
39396
39778
  return value.trim();
39397
39779
  }
39398
- var CONTENT_MANIFEST_SCHEMA_VERSION = 1, CONTENT_MANIFEST_MAX_BYTES, DEFAULT_CONTENT_MANIFEST_URL = "https://github.com/pajarori/farai-data/releases/latest/download/manifest.json", CONTENT_VERSION_PATTERN, SHA256_PATTERN;
39780
+ var CONTENT_MANIFEST_SCHEMA_VERSION = 1, CONTENT_MANIFEST_MAX_BYTES, DEFAULT_CONTENT_MANIFEST_URL = "https://github.com/pajarori/farai-data/releases/download/latest/manifest.json", CONTENT_VERSION_PATTERN, SHA256_PATTERN, SOURCE_COMMIT_PATTERN2;
39399
39781
  var init_manifest = __esm(() => {
39400
39782
  init_http_response();
39401
39783
  init_file_read();
39402
39784
  CONTENT_MANIFEST_MAX_BYTES = 256 * 1024;
39403
39785
  CONTENT_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
39404
39786
  SHA256_PATTERN = /^[a-f0-9]{64}$/;
39787
+ SOURCE_COMMIT_PATTERN2 = /^[a-f0-9]{40}$/;
39405
39788
  });
39406
39789
 
39407
39790
  // src/agent-content/updater.ts
@@ -39417,7 +39800,7 @@ __export(exports_updater, {
39417
39800
  CONTENT_MANIFEST_CACHE_TTL_MS: () => CONTENT_MANIFEST_CACHE_TTL_MS
39418
39801
  });
39419
39802
  import { createHash as createHash10, randomUUID as randomUUID4 } from "crypto";
39420
- import { closeSync as closeSync4, existsSync as existsSync18, lstatSync as lstatSync4, mkdirSync as mkdirSync7, openSync as openSync4, readSync as readSync2, readdirSync as readdirSync6, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync6, unlinkSync as unlinkSync6, writeSync } from "fs";
39803
+ import { closeSync as closeSync4, existsSync as existsSync18, lstatSync as lstatSync4, mkdirSync as mkdirSync7, openSync as openSync4, readSync as readSync2, readdirSync as readdirSync6, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync7, unlinkSync as unlinkSync6, writeSync } from "fs";
39421
39804
  import { dirname as dirname9, join as join23 } from "path";
39422
39805
  import { fileURLToPath as fileURLToPath2 } from "url";
39423
39806
  async function checkContentUpdate(options = {}) {
@@ -39592,6 +39975,9 @@ async function applyContentUpdate(manifest, manifestUrl, options = {}) {
39592
39975
  generatedAt: manifest.generatedAt,
39593
39976
  activatedAt: new Date().toISOString(),
39594
39977
  manifestUrl,
39978
+ ...manifest.sourceCommit ? {
39979
+ sourceCommit: manifest.sourceCommit
39980
+ } : {},
39595
39981
  ...previous && previous.version !== manifest.contentVersion ? {
39596
39982
  previousVersion: previous.version
39597
39983
  } : {},
@@ -39604,6 +39990,9 @@ async function applyContentUpdate(manifest, manifestUrl, options = {}) {
39604
39990
  pruneVersions(pointer);
39605
39991
  return {
39606
39992
  version: pointer.version,
39993
+ ...pointer.sourceCommit ? {
39994
+ sourceCommit: pointer.sourceCommit
39995
+ } : {},
39607
39996
  ...pointer.previousVersion ? {
39608
39997
  previousVersion: pointer.previousVersion
39609
39998
  } : {},
@@ -39637,6 +40026,9 @@ function rollbackContentUpdate() {
39637
40026
  generatedAt: manifest.generatedAt,
39638
40027
  activatedAt: new Date().toISOString(),
39639
40028
  manifestUrl: active.manifestUrl,
40029
+ ...manifest.sourceCommit ? {
40030
+ sourceCommit: manifest.sourceCommit
40031
+ } : {},
39640
40032
  previousVersion: active.version,
39641
40033
  knowledge: Boolean(manifest.knowledge && existsSync18(join23(previousPath, "knowledge.db"))),
39642
40034
  skills: Boolean(manifest.skills && existsSync18(join23(previousPath, "skills")))
@@ -39645,6 +40037,9 @@ function rollbackContentUpdate() {
39645
40037
  `, 384);
39646
40038
  return {
39647
40039
  version: next.version,
40040
+ ...next.sourceCommit ? {
40041
+ sourceCommit: next.sourceCommit
40042
+ } : {},
39648
40043
  previousVersion: next.previousVersion,
39649
40044
  knowledge: next.knowledge,
39650
40045
  skills: next.skills,
@@ -39696,6 +40091,10 @@ function contentUpdateDisabled(configured) {
39696
40091
  function isNewerManifest(manifest, active) {
39697
40092
  if (!active)
39698
40093
  return true;
40094
+ if (manifest.sourceCommit && active.sourceCommit)
40095
+ return manifest.sourceCommit !== active.sourceCommit;
40096
+ if (manifest.sourceCommit && !active.sourceCommit)
40097
+ return true;
39699
40098
  if (manifest.contentVersion === active.version)
39700
40099
  return false;
39701
40100
  const generated = Date.parse(manifest.generatedAt);
@@ -39771,7 +40170,7 @@ function acquireLock() {
39771
40170
  }
39772
40171
  function staleLock(path) {
39773
40172
  try {
39774
- const stats = statSync6(path);
40173
+ const stats = statSync7(path);
39775
40174
  const parsed = JSON.parse(readBoundedFileTextSyncNoFollow(path, 4 * 1024, "content update lock"));
39776
40175
  if (typeof parsed.pid === "number" && parsed.pid > 0) {
39777
40176
  try {
@@ -67344,16 +67743,18 @@ async function runContentUpdateCommand(parsed, workspace) {
67344
67743
  const status3 = contentStatus();
67345
67744
  if (!status3.active) {
67346
67745
  const knowledge = legacyKnowledgeDbPath();
67347
- console.log("content: bundled defaults");
67746
+ console.log("content: not installed");
67348
67747
  console.log("active release: none");
67349
67748
  console.log(`knowledge: ${existsSync26(knowledge) ? knowledge : "not installed"}`);
67350
- console.log("skills: bundled");
67749
+ console.log("skills: not installed");
67351
67750
  return 0;
67352
67751
  }
67353
67752
  console.log(`content: ${status3.active.version}`);
67753
+ if (status3.active.sourceCommit)
67754
+ console.log(`source commit: ${status3.active.sourceCommit}`);
67354
67755
  console.log(`activated: ${status3.active.activatedAt}`);
67355
67756
  console.log(`knowledge: ${status3.knowledgePath ?? "local fallback"}`);
67356
- console.log(`skills: ${status3.skillsPath ?? "bundled fallback"}`);
67757
+ console.log(`skills: ${status3.skillsPath ?? "not installed"}`);
67357
67758
  console.log(`available versions: ${status3.versions.join(", ") || "none"}`);
67358
67759
  return 0;
67359
67760
  }
@@ -67381,6 +67782,8 @@ async function runContentUpdateCommand(parsed, workspace) {
67381
67782
  function printContentUpdateStatus(status2) {
67382
67783
  if (status2.state === "update_available") {
67383
67784
  console.log(`content update available: ${status2.active?.version ?? "none"} -> ${status2.manifest?.contentVersion}`);
67785
+ if (status2.manifest?.sourceCommit)
67786
+ console.log(`source commit: ${status2.manifest.sourceCommit}`);
67384
67787
  return 0;
67385
67788
  }
67386
67789
  if (status2.state === "up_to_date") {
@@ -68145,7 +68548,7 @@ __export(exports_csi_suite, {
68145
68548
  loadCsiCampaignConfig: () => loadCsiCampaignConfig,
68146
68549
  generateCsiBenchmarkSuite: () => generateCsiBenchmarkSuite
68147
68550
  });
68148
- import { existsSync as existsSync27, readdirSync as readdirSync13, statSync as statSync7 } from "fs";
68551
+ import { existsSync as existsSync27, readdirSync as readdirSync13, statSync as statSync8 } from "fs";
68149
68552
  import { dirname as dirname12, isAbsolute as isAbsolute9, join as join35, relative as relative11, resolve as resolve11 } from "path";
68150
68553
  async function loadCsiCampaignConfig(path) {
68151
68554
  return normalizeCsiCampaignConfig(JSON.parse(await readBoundedFileText(path, CSI_CAMPAIGN_MAX_BYTES, "csi campaign config")));
@@ -68171,7 +68574,7 @@ async function generateCsiBenchmarkSuite(configInput, materialRoot) {
68171
68574
  if (config.isolation.backend === "host" && material.requiresTarget)
68172
68575
  throw new Error(`host csi challenge requires a live target and cannot run in host smoke mode: ${challenge.id}`);
68173
68576
  const promptPath = protectedPath(root, material.promptFile, `${challenge.id}.promptFile`);
68174
- if (!existsSync27(promptPath) || !statSync7(promptPath).isFile())
68577
+ if (!existsSync27(promptPath) || !statSync8(promptPath).isFile())
68175
68578
  throw new Error(`missing prompt file for csi challenge: ${challenge.id}`);
68176
68579
  const prompt = readBoundedFileTextSync(promptPath, CSI_PROMPT_MAX_BYTES, `csi prompt ${challenge.id}`).trim();
68177
68580
  if (!prompt)
@@ -68180,7 +68583,7 @@ async function generateCsiBenchmarkSuite(configInput, materialRoot) {
68180
68583
  const source = protectedPath(root, file.source, `${challenge.id}.files[${index}].source`);
68181
68584
  if (!existsSync27(source))
68182
68585
  throw new Error(`missing input for csi challenge ${challenge.id}: ${file.source}`);
68183
- if (statSync7(source).isDirectory() && !listFiles(source).length)
68586
+ if (statSync8(source).isDirectory() && !listFiles(source).length)
68184
68587
  throw new Error(`empty input directory for csi challenge ${challenge.id}: ${file.source}`);
68185
68588
  const digest2 = hashPath(source);
68186
68589
  if (file.sha256 && file.sha256.toLowerCase() !== digest2)
@@ -68200,14 +68603,14 @@ async function generateCsiBenchmarkSuite(configInput, materialRoot) {
68200
68603
  throw new Error(`missing required protected files for csi challenge ${challenge.id}: ${missing.join(", ")}`);
68201
68604
  }
68202
68605
  const executable = protectedPath(root, material.oracle.executable, `${challenge.id}.oracle.executable`);
68203
- if (!existsSync27(executable) || !statSync7(executable).isFile())
68606
+ if (!existsSync27(executable) || !statSync8(executable).isFile())
68204
68607
  throw new Error(`missing oracle executable for csi challenge: ${challenge.id}`);
68205
- if ((statSync7(executable).mode & 73) === 0)
68608
+ if ((statSync8(executable).mode & 73) === 0)
68206
68609
  throw new Error(`oracle executable is not executable for csi challenge: ${challenge.id}`);
68207
68610
  const antiCheatExecutable = material.antiCheat ? protectedPath(root, material.antiCheat.executable, `${challenge.id}.antiCheat.executable`) : undefined;
68208
- if (antiCheatExecutable && (!existsSync27(antiCheatExecutable) || !statSync7(antiCheatExecutable).isFile()))
68611
+ if (antiCheatExecutable && (!existsSync27(antiCheatExecutable) || !statSync8(antiCheatExecutable).isFile()))
68209
68612
  throw new Error(`missing anti-cheat executable for csi challenge: ${challenge.id}`);
68210
- if (antiCheatExecutable && (statSync7(antiCheatExecutable).mode & 73) === 0)
68613
+ if (antiCheatExecutable && (statSync8(antiCheatExecutable).mode & 73) === 0)
68211
68614
  throw new Error(`anti-cheat executable is not executable for csi challenge: ${challenge.id}`);
68212
68615
  if (config.isolation.backend === "docker" && !material.target)
68213
68616
  throw new Error(`docker csi challenge requires a pinned target image: ${challenge.id}`);
@@ -68416,7 +68819,7 @@ function protectedPath(root, path, name) {
68416
68819
  return resolved;
68417
68820
  }
68418
68821
  function listFiles(rootPath) {
68419
- if (!statSync7(rootPath).isDirectory())
68822
+ if (!statSync8(rootPath).isDirectory())
68420
68823
  return [rootPath];
68421
68824
  return readdirSync13(rootPath).flatMap((name) => listFiles(join35(rootPath, name)));
68422
68825
  }
@@ -68539,6 +68942,36 @@ class BenchmarkDockerLifecycle {
68539
68942
  this.runner = runner;
68540
68943
  }
68541
68944
  async start() {
68945
+ if (this.startPromise)
68946
+ return this.startPromise;
68947
+ const waitedForStop = Boolean(this.stopPromise);
68948
+ if (this.stopPromise)
68949
+ await this.stopPromise;
68950
+ if (!waitedForStop && this.stateValue?.started && !this.stateValue.cleaned && this.planValue) {
68951
+ return {
68952
+ backend: new KaliContainerBackend({
68953
+ workspace: this.workspace,
68954
+ image: DEFAULT_KALI_IMAGE,
68955
+ containerName: this.planValue.names.agent,
68956
+ processRunner: (command, args2) => this.runner(command, args2)
68957
+ }),
68958
+ state: this.stateValue,
68959
+ plan: this.planValue
68960
+ };
68961
+ }
68962
+ if (this.stateValue && !this.stateValue.cleaned) {
68963
+ const previous = await this.stopUnlocked();
68964
+ if (previous && !previous.cleaned)
68965
+ throw new Error("previous benchmark Docker resources could not be cleaned up");
68966
+ }
68967
+ this.startPromise = this.startUnlocked();
68968
+ try {
68969
+ return await this.startPromise;
68970
+ } finally {
68971
+ this.startPromise = undefined;
68972
+ }
68973
+ }
68974
+ async startUnlocked() {
68542
68975
  const processRunner = (command, args2) => this.runner(command, args2);
68543
68976
  const image = await new KaliContainerBackend({
68544
68977
  workspace: this.workspace,
@@ -68546,14 +68979,17 @@ class BenchmarkDockerLifecycle {
68546
68979
  processRunner
68547
68980
  }).resolveImage();
68548
68981
  if (!image.exists)
68549
- throw new Error(`benchmark agent image is missing: ${DEFAULT_KALI_IMAGE}`);
68982
+ throw new Error(image.error ?? `benchmark agent image is missing: ${DEFAULT_KALI_IMAGE}`);
68983
+ if (image.error)
68984
+ throw new Error(image.error);
68550
68985
  const agentImageId = image.id?.trim() ?? "";
68551
68986
  if (!/^sha256:[a-f0-9]{64}$/i.test(agentImageId))
68552
68987
  throw new Error(`docker returned an unpinned agent image id: ${agentImageId || "empty"}`);
68553
68988
  const agentImageContract = image.contract?.trim() ?? "";
68554
68989
  if (agentImageContract !== KALI_IMAGE_CONTRACT)
68555
68990
  throw new Error(`benchmark agent image does not satisfy the current capability contract: ${agentImageContract || "missing"}`);
68556
- const plan = buildBenchmarkDockerPlan(this.manifest, this.workspace, this.runId, agentImageId);
68991
+ const targetImage = await resolveTargetImage(this.manifest, this.runner);
68992
+ const plan = buildBenchmarkDockerPlan(this.manifest, this.workspace, this.runId, agentImageId, targetImage);
68557
68993
  this.planValue = plan;
68558
68994
  const state = {
68559
68995
  network: plan.names.network,
@@ -68561,6 +68997,7 @@ class BenchmarkDockerLifecycle {
68561
68997
  agentContainer: plan.names.agent,
68562
68998
  agentImageId,
68563
68999
  agentImageContract,
69000
+ targetImage,
68564
69001
  started: false,
68565
69002
  antiCheatApplied: false,
68566
69003
  cleaned: false,
@@ -68592,27 +69029,49 @@ class BenchmarkDockerLifecycle {
68592
69029
  };
68593
69030
  } catch (error) {
68594
69031
  state.errors.push(error instanceof Error ? error.message : String(error));
68595
- await this.stop();
69032
+ await this.stopUnlocked();
68596
69033
  throw error;
68597
69034
  }
68598
69035
  }
68599
69036
  async stop() {
69037
+ if (this.startPromise)
69038
+ await this.startPromise.catch(() => {
69039
+ return;
69040
+ });
69041
+ if (this.stopPromise)
69042
+ return this.stopPromise;
69043
+ this.stopPromise = this.stopUnlocked();
69044
+ try {
69045
+ return await this.stopPromise;
69046
+ } finally {
69047
+ this.stopPromise = undefined;
69048
+ }
69049
+ }
69050
+ async stopUnlocked() {
68600
69051
  const state = this.stateValue;
68601
69052
  const plan = this.planValue;
68602
69053
  if (!state || !plan || state.cleaned)
68603
69054
  return state;
68604
- const agentState = await this.inspectState(plan.names.agent);
68605
- const targetState = await this.inspectState(plan.names.target);
69055
+ const [agentState, targetState] = await Promise.all([this.inspectState(plan.names.agent), this.inspectState(plan.names.target)]);
68606
69056
  if (agentState)
68607
69057
  state.agentState = agentState;
68608
69058
  if (targetState)
68609
69059
  state.targetState = targetState;
69060
+ let cleaned = true;
68610
69061
  for (const args2 of plan.cleanup) {
68611
- const result = await this.runner("docker", args2);
68612
- if (result.exitCode !== 0 && result.stderr.trim())
68613
- state.errors.push(result.stderr.trim().slice(0, 500));
69062
+ try {
69063
+ const result = await this.runner("docker", args2);
69064
+ if (result.exitCode !== 0 && !resourceDoesNotExist(result)) {
69065
+ cleaned = false;
69066
+ if (result.stderr.trim())
69067
+ state.errors.push(result.stderr.trim().slice(0, 500));
69068
+ }
69069
+ } catch (error) {
69070
+ cleaned = false;
69071
+ state.errors.push(error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500));
69072
+ }
68614
69073
  }
68615
- state.cleaned = true;
69074
+ state.cleaned = cleaned;
68616
69075
  return state;
68617
69076
  }
68618
69077
  async requiredDocker(args2, operation) {
@@ -68621,10 +69080,10 @@ class BenchmarkDockerLifecycle {
68621
69080
  throw new Error(result.stderr || `failed to ${operation}`);
68622
69081
  }
68623
69082
  async inspectState(name) {
68624
- const result = await this.runner("docker", ["inspect", "--format", "{{json .State}}", name]);
68625
- if (result.exitCode !== 0)
68626
- return;
68627
69083
  try {
69084
+ const result = await this.runner("docker", ["inspect", "--format", "{{json .State}}", name]);
69085
+ if (result.exitCode !== 0)
69086
+ return;
68628
69087
  const state = JSON.parse(result.stdout);
68629
69088
  if (typeof state.Running !== "boolean" || typeof state.ExitCode !== "number")
68630
69089
  return;
@@ -68637,7 +69096,7 @@ class BenchmarkDockerLifecycle {
68637
69096
  }
68638
69097
  }
68639
69098
  }
68640
- function buildBenchmarkDockerPlan(manifest, workspace, runId, agentImageId) {
69099
+ function buildBenchmarkDockerPlan(manifest, workspace, runId, agentImageId, targetImageOverride) {
68641
69100
  if (manifest.isolation.backend !== "docker")
68642
69101
  throw new Error("benchmark docker plan requires isolation.backend=docker");
68643
69102
  if (manifest.isolation.network !== "target_only" || manifest.isolation.internet !== "disabled") {
@@ -68664,12 +69123,13 @@ function buildBenchmarkDockerPlan(manifest, workspace, runId, agentImageId) {
68664
69123
  throw new Error("docker benchmark diskMb is not enforceable for a bind-mounted scratch workspace");
68665
69124
  const common = ["--security-opt", "no-new-privileges:true", ...resources?.pids ? ["--pids-limit", String(resources.pids)] : []];
68666
69125
  const resourceArgs = [...resources?.cpus ? ["--cpus", String(resources.cpus)] : [], ...resources?.memoryMb ? ["--memory", `${resources.memoryMb}m`] : []];
68667
- const targetImage = pinnedImage(manifest.challenge.targetImage, manifest.challenge.targetImageDigest);
69126
+ const targetImage = targetImageOverride ?? pinnedImage(manifest.challenge.targetImage, manifest.challenge.targetImageDigest);
68668
69127
  const targetStart = ["run", "-d", "--name", names.target, "--network", names.network, "--network-alias", "target", "--cap-drop", "ALL", "--cap-add", "NET_BIND_SERVICE", ...common, ...resourceArgs, targetImage, ...manifest.challenge.targetCommand ?? []];
68669
69128
  const resolvedWorkspace = resolve12(workspace);
68670
69129
  const agentStart = ["run", "-d", "--name", names.agent, "--network", names.network, "--workdir", "/workspace", "--volume", `${resolvedWorkspace}:/workspace:rw`, "--volume", "/workspace/.farai", "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,size=512m", "--tmpfs", "/root:rw,nosuid,nodev,size=256m", "--tmpfs", "/run:rw,nosuid,nodev,size=64m", "--cap-drop", "ALL", "--cap-add", "NET_ADMIN", "--cap-add", "NET_RAW", ...common, ...resourceArgs, agentImageId, "sleep", "infinity"];
68671
69130
  return {
68672
69131
  names,
69132
+ targetImage,
68673
69133
  networkCreate: ["network", "create", "--internal", "--label", "org.farai.benchmark=true", names.network],
68674
69134
  targetStart,
68675
69135
  agentStart,
@@ -68686,6 +69146,51 @@ function buildBenchmarkDockerPlan(manifest, workspace, runId, agentImageId) {
68686
69146
  cleanup: [["rm", "-f", "-v", names.agent], ["rm", "-f", "-v", names.target], ["network", "rm", names.network]]
68687
69147
  };
68688
69148
  }
69149
+ async function resolveTargetImage(manifest, runner) {
69150
+ if (!manifest.challenge.targetImage || !manifest.challenge.targetImageDigest)
69151
+ throw new Error("docker benchmark requires a pinned target image");
69152
+ const pinned = pinnedImage(manifest.challenge.targetImage, manifest.challenge.targetImageDigest);
69153
+ const exact = await runner("docker", ["image", "inspect", pinned]);
69154
+ if (exact.exitCode === 0)
69155
+ return imageIdFromInspect(exact.stdout) ?? pinned;
69156
+ const base = manifest.challenge.targetImage.split("@")[0];
69157
+ const tagged = await runner("docker", ["image", "inspect", base]);
69158
+ if (tagged.exitCode === 0) {
69159
+ const imageId = imageIdForDigest(tagged.stdout, pinned);
69160
+ if (imageId)
69161
+ return imageId;
69162
+ }
69163
+ const details = [exact.stderr, tagged.stderr].map((value) => value.trim()).find(Boolean);
69164
+ throw new Error(`benchmark target image is unavailable locally: ${pinned}; build or load the pinned target image before running the benchmark${details ? ` (${details.slice(0, 300)})` : ""}`);
69165
+ }
69166
+ function imageIdForDigest(raw, pinned) {
69167
+ try {
69168
+ const images = JSON.parse(raw);
69169
+ const image = images[0];
69170
+ if (!image || !Array.isArray(image.RepoDigests))
69171
+ return;
69172
+ const [repository, digest2] = pinned.split("@");
69173
+ const repositoryWithoutTag = repository?.replace(/:[^/:]+$/, "");
69174
+ const matches2 = image.RepoDigests.some((value) => typeof value === "string" && (value.toLowerCase() === pinned.toLowerCase() || repositoryWithoutTag && digest2 && value.toLowerCase() === `${repositoryWithoutTag}@${digest2}`.toLowerCase()));
69175
+ if (!matches2 || typeof image.Id !== "string" || !/^sha256:[a-f0-9]{64}$/i.test(image.Id))
69176
+ return;
69177
+ return image.Id;
69178
+ } catch {
69179
+ return;
69180
+ }
69181
+ }
69182
+ function imageIdFromInspect(raw) {
69183
+ try {
69184
+ const id2 = JSON.parse(raw)[0]?.Id;
69185
+ return typeof id2 === "string" && /^sha256:[a-f0-9]{64}$/i.test(id2) ? id2 : undefined;
69186
+ } catch {
69187
+ return;
69188
+ }
69189
+ }
69190
+ function resourceDoesNotExist(result) {
69191
+ return /no such (container|network|object)|(?:container|network)\s+[^\n]*\bnot found\b/i.test(`${result.stdout}
69192
+ ${result.stderr}`);
69193
+ }
68689
69194
  function pinnedImage(image, digest2) {
68690
69195
  const normalizedDigest = digest2.startsWith("sha256:") ? digest2 : `sha256:${digest2}`;
68691
69196
  if (!/^sha256:[a-f0-9]{64}$/i.test(normalizedDigest))
@@ -70377,12 +70882,15 @@ async function setup(args2) {
70377
70882
  console.log("[*] skipping Docker image build");
70378
70883
  }
70379
70884
  if (!parsed.skipKnowledge) {
70380
- console.log("[*] building Farai knowledge base");
70381
- const code = await (await Promise.resolve().then(() => (init_command(), exports_command))).runKbCommand(["build", "all"]);
70382
- if (code !== 0) {
70383
- process.exitCode = code;
70384
- console.error("[!] knowledge base build failed");
70385
- return;
70885
+ const contentInstalled = await syncContentForSetup(process.cwd());
70886
+ if (!contentInstalled) {
70887
+ console.log("[*] building Farai knowledge base");
70888
+ const code = await (await Promise.resolve().then(() => (init_command(), exports_command))).runKbCommand(["build", "all"]);
70889
+ if (code !== 0) {
70890
+ process.exitCode = code;
70891
+ console.error("[!] knowledge base build failed");
70892
+ return;
70893
+ }
70386
70894
  }
70387
70895
  } else {
70388
70896
  console.log("[*] skipping knowledge base build");
@@ -70390,6 +70898,30 @@ async function setup(args2) {
70390
70898
  console.log("[+] setup complete");
70391
70899
  console.log("[+] run `farai doctor` to verify the environment");
70392
70900
  }
70901
+ async function syncContentForSetup(workspace) {
70902
+ const {
70903
+ applyContentUpdate: applyContentUpdate2,
70904
+ checkContentUpdate: checkContentUpdate2,
70905
+ contentStatus: contentStatus2
70906
+ } = await Promise.resolve().then(() => (init_updater(), exports_updater));
70907
+ try {
70908
+ const status2 = await checkContentUpdate2({
70909
+ workspace,
70910
+ force: true
70911
+ });
70912
+ if (status2.state === "update_available" && status2.manifest) {
70913
+ console.log(`[*] syncing Farai content ${status2.manifest.contentVersion}`);
70914
+ const applied = await applyContentUpdate2(status2.manifest, status2.manifestUrl);
70915
+ const parts = [applied.knowledge ? "knowledge" : undefined, applied.skills ? "skills" : undefined].filter(Boolean).join(" + ");
70916
+ console.log(`[+] content: ${applied.version}${parts ? ` (${parts})` : ""}`);
70917
+ } else if (status2.state === "error") {
70918
+ console.error(`[!] content sync unavailable: ${status2.error ?? "unknown error"}`);
70919
+ }
70920
+ } catch (error) {
70921
+ console.error(`[!] content sync failed: ${error instanceof Error ? error.message : String(error)}`);
70922
+ }
70923
+ return Boolean(contentStatus2().active?.knowledge);
70924
+ }
70393
70925
  async function models(args2 = []) {
70394
70926
  const parsed = parseModelArguments(args2);
70395
70927
  ensureDefaultUserConfig();
@@ -70727,5 +71259,5 @@ Examples:
70727
71259
  `);
70728
71260
  }
70729
71261
 
70730
- //# debugId=59D0DF40CE3D0BDB64756E2164756E21
71262
+ //# debugId=ABD822216062EDDD64756E2164756E21
70731
71263
  //# sourceMappingURL=index.js.map