@zhivex-ai/harness 1.4.1 → 1.6.2

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -1
  3. package/SUPPORT.md +1 -1
  4. package/contracts/stable-api-signatures.json +7 -7
  5. package/dist/acp-cli.js +10 -10
  6. package/dist/approvals/approval-diff.d.ts +2 -1
  7. package/dist/approvals/approval-preview.d.ts +1 -1
  8. package/dist/approvals/auto-approve.d.ts +137 -0
  9. package/dist/audit/index.js +6 -6
  10. package/dist/{audit-pack-e4vaq7np.js → audit-pack-fckpqfsb.js} +3 -3
  11. package/dist/{cli-1cf4dcnj.js → cli-84en174d.js} +14 -8
  12. package/dist/{cli-entry-3s63zn4w.js → cli-entry-10th5awp.js} +16 -15
  13. package/dist/{cli-entry-a890x3kq.js → cli-entry-4g2ewcnw.js} +15 -5
  14. package/dist/{cli-entry-7f0cen32.js → cli-entry-6amx4mcc.js} +209 -68
  15. package/dist/{cli-entry-rgs6bxmy.js → cli-entry-z1pbjrg4.js} +2 -2
  16. package/dist/cli.js +2 -2
  17. package/dist/compat/code-support.js +8 -8
  18. package/dist/engine/acp.js +11 -11
  19. package/dist/engine/client.js +10 -10
  20. package/dist/engine/desktop-state-v1.js +6 -6
  21. package/dist/engine/index.js +15 -15
  22. package/dist/engine/mcp-stdio-v1.js +5 -5
  23. package/dist/engine/service.js +11 -11
  24. package/dist/hostile-repository-demo.js +235 -84
  25. package/dist/{index-mv8sb1ns.js → index-1pn0ecs4.js} +55 -13
  26. package/dist/{index-dtb2c6ds.js → index-2r4f0p4n.js} +2 -2
  27. package/dist/{index-6599mpmy.js → index-4mn6m3bq.js} +3 -3
  28. package/dist/{index-xdbkcccf.js → index-6dxrbnxm.js} +2 -2
  29. package/dist/{index-crta5wzv.js → index-74f9yhfh.js} +4 -4
  30. package/dist/{index-f6mqa5gk.js → index-amjttjaj.js} +11 -39
  31. package/dist/{index-wm9xeq1n.js → index-bt2p8x7r.js} +1 -1
  32. package/dist/{index-sk3cap35.js → index-eps3c3g6.js} +1 -1
  33. package/dist/{index-zxg4ve4j.js → index-fqqz1yt8.js} +77 -4
  34. package/dist/{index-02pqtbbg.js → index-paf7z8kn.js} +10 -10
  35. package/dist/{index-0zs2dz26.js → index-pf73y4gt.js} +18 -9
  36. package/dist/{index-74kkpyqk.js → index-rgqzdgv6.js} +12 -6
  37. package/dist/{index-4fx5f3k5.js → index-rz7wmp71.js} +16 -15
  38. package/dist/{index-d9zkmhz3.js → index-w0m1nckc.js} +127 -28
  39. package/dist/{index-sstgcsr0.js → index-yx52bt4a.js} +1 -1
  40. package/dist/{index-j6zxnc8m.js → index-zrz4rs7p.js} +5 -5
  41. package/dist/index.js +14 -14
  42. package/dist/integrations/mcp-stdio-tools.d.ts +4 -1
  43. package/dist/providers/bedrock.d.ts +1 -1
  44. package/dist/review/reveal-invisible.d.ts +13 -0
  45. package/dist/runtime/budget-ledger-scope.d.ts +14 -0
  46. package/dist/service-cli.js +11 -11
  47. package/dist/{service-client-cli-eqrdc9zq.js → service-client-cli-pp5z4dmf.js} +4 -4
  48. package/dist/tools/workspace.d.ts +2 -2
  49. package/dist/verify-historical-migrations.js +240 -86
  50. package/dist/workspace/edit-contracts.d.ts +2 -2
  51. package/dist/workspace/path-policy.d.ts +2 -1
  52. package/dist/workspace/text-integrity.d.ts +19 -0
  53. package/dist/workspace/workspace.d.ts +21 -3
  54. package/dist/zhx.js +2 -2
  55. package/docs/AUTO_APPROVE.md +51 -0
  56. package/docs/AZURE_OPENAI.md +4 -4
  57. package/docs/BEDROCK.md +4 -4
  58. package/docs/CLI.md +13 -0
  59. package/docs/CODE_RELEASE.md +36 -6
  60. package/docs/CODE_WEB_TASKS.md +1 -1
  61. package/docs/CREDENTIALS.md +1 -1
  62. package/docs/DURABLE_OPERATIONS.md +1 -1
  63. package/docs/ENGINE_API.md +1 -1
  64. package/docs/EXTENSIBILITY.md +7 -3
  65. package/docs/FIRST_USE.md +6 -6
  66. package/docs/MINIMUM_TASK_AUTHORITY.md +5 -5
  67. package/docs/PROJECT_MEMORY.md +1 -1
  68. package/docs/PUBLIC_SECURITY.md +14 -1
  69. package/docs/REPOSITORY_EDITING.md +1 -1
  70. package/docs/TOOL_POLICY.md +7 -1
  71. package/docs/VERTEX.md +2 -2
  72. package/docs/WEB_LIMITS.md +2 -2
  73. package/docs/adr/0002-isolated-mcp-stdio.md +2 -2
  74. package/evaluations/representative-assembly-matrix.json +37 -1
  75. package/examples/README.md +3 -3
  76. package/package.json +16 -15
@@ -5567,8 +5567,8 @@ var quarantineFileInputSchema = z11.strictObject({
5567
5567
  });
5568
5568
  var restoreFileInputSchema = z11.strictObject({
5569
5569
  quarantineId: z11.string().min(1).max(200).regex(/^[A-Za-z0-9._:-]+$/),
5570
- destination: workspaceFilePathSchema.optional(),
5571
- expectedDigest: fileDigestSchema.optional()
5570
+ destination: workspaceFilePathSchema.describe("Required workspace path for the quarantined bytes. Restore does not fall back to the original path."),
5571
+ expectedDigest: fileDigestSchema.describe("Required sha256 of the quarantined bytes the operator reviewed.")
5572
5572
  });
5573
5573
  var mutationOperationSchema = z11.enum(["create", "update", "move", "quarantine", "restore"]);
5574
5574
  var mutationAuditEntrySchema = z11.strictObject({
@@ -5905,13 +5905,62 @@ var runPortableProcess = async (command, options = {}) => {
5905
5905
  }
5906
5906
  };
5907
5907
 
5908
+ // src/workspace/text-integrity.ts
5909
+ var controlLabel = (code) => code === 0 ? "NUL byte" : `U+${code.toString(16).padStart(4, "0")}`;
5910
+ var firstDisallowedControl = (text2) => {
5911
+ let line = 1;
5912
+ for (let index = 0;index < text2.length; index += 1) {
5913
+ const code = text2.charCodeAt(index);
5914
+ if (code === 10) {
5915
+ line += 1;
5916
+ continue;
5917
+ }
5918
+ if (code === 9 || code === 13)
5919
+ continue;
5920
+ if (code < 32 || code === 127 || code >= 128 && code <= 159)
5921
+ return { line, label: controlLabel(code) };
5922
+ }
5923
+ return;
5924
+ };
5925
+ var firstDisallowedControlBytes = (bytes) => {
5926
+ let line = 1;
5927
+ for (let index = 0;index < bytes.length; index += 1) {
5928
+ const code = bytes[index] ?? 0;
5929
+ if (code === 10) {
5930
+ line += 1;
5931
+ continue;
5932
+ }
5933
+ if (code === 9 || code === 13)
5934
+ continue;
5935
+ if (code < 32 || code === 127)
5936
+ return { line, label: controlLabel(code) };
5937
+ }
5938
+ return;
5939
+ };
5940
+ var refuseControlMessage = (hit) => `Refusing to write a control byte into a text file: ${hit.label} at L${hit.line}. The file was not changed. Remove the control byte.`;
5941
+ var binaryAfterEditMessage = (hit) => `file is binary after edit: ${hit.label} at L${hit.line}. The edit was reverted. Read the file again before retrying.`;
5942
+ var textGuards = {
5943
+ beforeWrite(content, allowControlBytes) {
5944
+ if (allowControlBytes)
5945
+ return;
5946
+ const problem = firstDisallowedControl(content);
5947
+ if (problem)
5948
+ throw new HarnessWorkspaceError(refuseControlMessage(problem));
5949
+ },
5950
+ afterWrite(bytes, allowControlBytes) {
5951
+ if (allowControlBytes)
5952
+ return;
5953
+ return firstDisallowedControlBytes(bytes);
5954
+ }
5955
+ };
5956
+
5908
5957
  // src/workspace/path-policy.ts
5909
5958
  var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
5910
5959
  var isSensitiveName = (name) => {
5911
- const normalized = name.toLocaleLowerCase();
5960
+ const normalized = name.toLowerCase();
5912
5961
  return normalized === ".env" || normalized.startsWith(".env.") && normalized !== ".env.example" || normalized === ".npmrc" || normalized === "id_rsa" || normalized === "id_ed25519" || [".key", ".pem", ".p12", ".pfx"].some((extension) => normalized.endsWith(extension));
5913
5962
  };
5914
- var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
5963
+ var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
5915
5964
 
5916
5965
  // src/workspace/workspace.ts
5917
5966
  var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
@@ -6191,7 +6240,7 @@ class Workspace {
6191
6240
  throw new HarnessWorkspaceError(`The path escapes the workspace: ${relativePath}`);
6192
6241
  const relative = wirePath(path11.relative(this.root, candidate));
6193
6242
  if (relative && isHardIgnored(relative)) {
6194
- const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
6243
+ const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
6195
6244
  throw new HarnessWorkspaceError(`The path is protected by the harness policy: ${blocked}`);
6196
6245
  }
6197
6246
  return candidate;
@@ -6234,8 +6283,11 @@ class Workspace {
6234
6283
  maxBytes: MAX_FILE_BYTES
6235
6284
  });
6236
6285
  const { contents } = file;
6237
- if (!allowBinary && contents.includes(0))
6238
- throw new HarnessWorkspaceError("The file appears to be binary and cannot be read as text.");
6286
+ if (!allowBinary && contents.includes(0)) {
6287
+ const hit = firstDisallowedControlBytes(contents);
6288
+ const where = hit ? `: ${hit.label} at L${hit.line}` : "";
6289
+ throw new HarnessWorkspaceError(`The file appears to be binary and cannot be read as text${where}.`);
6290
+ }
6239
6291
  return {
6240
6292
  path: wirePath(path11.relative(this.root, safe.path)),
6241
6293
  absolutePath: safe.path,
@@ -6902,19 +6954,23 @@ class Workspace {
6902
6954
  if (position < 0 || text2.indexOf(edit.oldText, position + 1) >= 0) {
6903
6955
  throw new HarnessWorkspaceError("Replacement requires exactly one literal match; include more surrounding context.");
6904
6956
  }
6905
- return [{ path: edit.path, expectedDigest: edit.expectedDigest, content: text2.slice(0, position) + edit.newText + text2.slice(position + edit.oldText.length) }];
6957
+ return [{
6958
+ path: edit.path,
6959
+ expectedDigest: edit.expectedDigest,
6960
+ content: text2.slice(0, position) + edit.newText + text2.slice(position + edit.oldText.length)
6961
+ }];
6906
6962
  }
6907
- async applyReplacement(input) {
6963
+ async applyReplacement(input, host = {}) {
6908
6964
  const changes = await this.replacementChanges(input);
6909
- return this.applyPatch({ proposalId: createEditProposal({ changes }).proposalId, changes });
6965
+ return this.applyPatch({ proposalId: createEditProposal({ changes }).proposalId, changes }, host);
6910
6966
  }
6911
- async applyPatch(input) {
6912
- return this.applyPatchWithModes(input, new Map);
6967
+ async applyPatch(input, host = {}) {
6968
+ return this.applyPatchWithModes(input, new Map, undefined, host);
6913
6969
  }
6914
- async applyPatchWithModes(input, modes, assertActive) {
6915
- return withWorkspaceMutation(this.root, () => this.applyPatchWithModesLocked(input, modes, assertActive));
6970
+ async applyPatchWithModes(input, modes, assertActive, host = {}) {
6971
+ return withWorkspaceMutation(this.root, () => this.applyPatchWithModesLocked(input, modes, assertActive, host));
6916
6972
  }
6917
- async applyPatchWithModesLocked(input, modes, assertActive) {
6973
+ async applyPatchWithModesLocked(input, modes, assertActive, host = {}) {
6918
6974
  const proposal = validateEditProposal(input);
6919
6975
  this.checkAuditCapacity(proposal.changes.length);
6920
6976
  const targetPaths = new Set(proposal.changes.map((change) => change.path));
@@ -6959,6 +7015,7 @@ class Workspace {
6959
7015
  throw new HarnessWorkspaceError(`Stale patch mode rejected for ${change.path}: expected ${modeBinding.beforeMode.toString(8)}, found ${before.mode.toString(8)}.`);
6960
7016
  }
6961
7017
  }
7018
+ textGuards.beforeWrite(change.content, host.allowControlBytes === true);
6962
7019
  const temporary = await this.stageFile(safe.path, change.content, modeBinding?.afterMode ?? before?.mode ?? 420);
6963
7020
  prepared.push({
6964
7021
  change,
@@ -6998,6 +7055,13 @@ class Workspace {
6998
7055
  await unlink(item.temporary);
6999
7056
  }
7000
7057
  committed.push(item);
7058
+ const published = await readRegularFileNoFollow(item.target, {
7059
+ label: "The published file",
7060
+ maxBytes: MAX_FILE_BYTES
7061
+ });
7062
+ const corrupt = textGuards.afterWrite(published.contents, host.allowControlBytes === true);
7063
+ if (corrupt)
7064
+ throw new HarnessWorkspaceError(binaryAfterEditMessage(corrupt));
7001
7065
  await assertActive?.();
7002
7066
  }
7003
7067
  } catch (error) {
@@ -7174,30 +7238,65 @@ class Workspace {
7174
7238
  const audit = await this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
7175
7239
  return { quarantineId, path: parsed.path, digest: source.digest, audit };
7176
7240
  }
7177
- async restoreQuarantined(input) {
7178
- return withWorkspaceMutation(this.root, () => this.restoreQuarantinedLocked(input));
7179
- }
7180
- async restoreQuarantinedLocked(input) {
7181
- const parsed = restoreFileInputSchema.parse(input);
7182
- this.checkAuditCapacity();
7241
+ async readRestorableQuarantine(quarantineId) {
7183
7242
  const directory = await this.secureQuarantineDirectory(false);
7184
7243
  let manifest;
7185
7244
  try {
7186
- const manifestFile = await this.readInternalRegularFile(path11.join(directory, `${parsed.quarantineId}.json`), "Quarantine manifest", 64 * 1024);
7245
+ const manifestFile = await this.readInternalRegularFile(path11.join(directory, `${quarantineId}.json`), "Quarantine manifest", 64 * 1024);
7187
7246
  manifest = JSON.parse(manifestFile.contents.toString("utf8"));
7188
7247
  } catch {
7189
- throw new HarnessWorkspaceError(`Quarantine entry was not found: ${parsed.quarantineId}.`);
7248
+ throw new HarnessWorkspaceError(`Quarantine entry was not found: ${quarantineId}.`);
7190
7249
  }
7191
- if (manifest.schemaVersion !== 1 || manifest.quarantineId !== parsed.quarantineId || manifest.status !== "quarantined") {
7192
- throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${parsed.quarantineId}.`);
7250
+ if (manifest.schemaVersion !== 1 || manifest.quarantineId !== quarantineId || manifest.status !== "quarantined") {
7251
+ throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${quarantineId}.`);
7193
7252
  }
7194
- const dataPath = path11.join(directory, `${parsed.quarantineId}.data`);
7253
+ const dataPath = path11.join(directory, `${quarantineId}.data`);
7195
7254
  const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
7196
7255
  const digest3 = digestBytes(dataFile.contents);
7197
- if (digest3 !== manifest.digest || parsed.expectedDigest && parsed.expectedDigest !== digest3) {
7256
+ if (digest3 !== manifest.digest) {
7257
+ throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${quarantineId}.`);
7258
+ }
7259
+ return { manifest, dataPath, dataFile, digest: digest3 };
7260
+ }
7261
+ async previewRestore(input) {
7262
+ const parsed = restoreFileInputSchema.parse(input);
7263
+ const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
7264
+ if (parsed.expectedDigest !== quarantined.digest) {
7198
7265
  throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
7199
7266
  }
7200
- const destinationPath = parsed.destination ?? manifest.originalPath;
7267
+ const destination = await this.safePath(parsed.destination, { allowMissing: true });
7268
+ if (destination.exists)
7269
+ throw new HarnessWorkspaceError(`Restore destination already exists: ${parsed.destination}.`);
7270
+ let after;
7271
+ try {
7272
+ after = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(quarantined.dataFile.contents);
7273
+ } catch {
7274
+ throw new HarnessWorkspaceError("Preview requires valid UTF-8 text.");
7275
+ }
7276
+ return {
7277
+ proposalId: parsed.expectedDigest,
7278
+ files: [{
7279
+ path: parsed.destination,
7280
+ expectedDigest: parsed.expectedDigest,
7281
+ before: null,
7282
+ after,
7283
+ afterDigest: quarantined.digest,
7284
+ operation: "restore"
7285
+ }]
7286
+ };
7287
+ }
7288
+ async restoreQuarantined(input) {
7289
+ return withWorkspaceMutation(this.root, () => this.restoreQuarantinedLocked(input));
7290
+ }
7291
+ async restoreQuarantinedLocked(input) {
7292
+ const parsed = restoreFileInputSchema.parse(input);
7293
+ this.checkAuditCapacity();
7294
+ const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
7295
+ if (parsed.expectedDigest !== quarantined.digest) {
7296
+ throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
7297
+ }
7298
+ const { manifest, dataPath, dataFile, digest: digest3 } = quarantined;
7299
+ const destinationPath = parsed.destination;
7201
7300
  const destination = await this.safePath(destinationPath, { allowMissing: true });
7202
7301
  if (destination.exists)
7203
7302
  throw new HarnessWorkspaceError(`Restore destination already exists: ${destinationPath}.`);
@@ -8420,7 +8519,7 @@ var importPatch = async (runId, expectedPatchId, host, base, current2, maxFileWr
8420
8519
  const rollbackErrors = [];
8421
8520
  for (const item of [...quarantined].reverse()) {
8422
8521
  try {
8423
- await host.restoreQuarantined({ quarantineId: item.quarantineId, expectedDigest: item.digest });
8522
+ await host.restoreQuarantined({ quarantineId: item.quarantineId, destination: item.path, expectedDigest: item.digest });
8424
8523
  } catch (rollbackError) {
8425
8524
  rollbackErrors.push(rollbackError);
8426
8525
  }
@@ -9718,6 +9817,79 @@ import { createAgentRunLedger } from "@zhivex-ai/agents/control-plane";
9718
9817
  import { AsyncLocalStorage as AsyncLocalStorage5 } from "node:async_hooks";
9719
9818
  import { randomUUID as randomUUID6 } from "node:crypto";
9720
9819
  import { z as z18 } from "zod";
9820
+
9821
+ // src/runtime/budget-ledger-scope.ts
9822
+ var BUDGET_LEDGER_NAMESPACE = "__zhivex_budget__";
9823
+ var BUDGET_OWNER_NAMESPACE_KEY = "budgetOwnerNamespace";
9824
+ var isReservedBudgetNamespace = (namespace) => namespace === BUDGET_LEDGER_NAMESPACE || (namespace?.startsWith(`${BUDGET_LEDGER_NAMESPACE}:`) ?? false);
9825
+ var budgetLedgerNamespace = (namespace) => {
9826
+ if (!namespace || isReservedBudgetNamespace(namespace)) {
9827
+ throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
9828
+ }
9829
+ return `${BUDGET_LEDGER_NAMESPACE}:${namespace}`;
9830
+ };
9831
+ var budgetLedgerScope = (scope) => {
9832
+ if (scope.namespace && isReservedBudgetNamespace(scope.namespace)) {
9833
+ throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
9834
+ }
9835
+ return {
9836
+ tenantId: scope.tenantId,
9837
+ ...scope.userId ? { userId: scope.userId } : {},
9838
+ namespace: scope.namespace ? budgetLedgerNamespace(scope.namespace) : `${BUDGET_LEDGER_NAMESPACE}:`
9839
+ };
9840
+ };
9841
+ var sameIdentity = (left, right) => left?.tenantId === right.tenantId && (left.userId ?? undefined) === (right.userId ?? undefined);
9842
+ var bindBudgetLedgerStore = (primary, scope, budget = primary) => {
9843
+ const bound = budgetLedgerScope(scope);
9844
+ const ownerNamespace = scope.namespace ?? "";
9845
+ const isSdkBudget = (value) => sameIdentity(value, bound) && value?.namespace === BUDGET_LEDGER_NAMESPACE;
9846
+ const isBoundBudget = (value) => sameIdentity(value, bound) && value?.namespace === bound.namespace;
9847
+ const assertId = (id) => {
9848
+ if (!/^budget_[a-f0-9]{64}$/.test(id))
9849
+ throw new HarnessStateConflictError("Invalid budget ledger identity.");
9850
+ };
9851
+ const bindState = (state) => {
9852
+ const metadata = { ...state.metadata ?? {} };
9853
+ const owner = metadata[BUDGET_OWNER_NAMESPACE_KEY];
9854
+ if (owner !== undefined && owner !== ownerNamespace) {
9855
+ throw new HarnessStateConflictError("Budget ledger is bound to another namespace.");
9856
+ }
9857
+ return {
9858
+ ...state,
9859
+ scope: bound,
9860
+ metadata: { ...metadata, [BUDGET_OWNER_NAMESPACE_KEY]: ownerNamespace }
9861
+ };
9862
+ };
9863
+ const assertStored = (state) => {
9864
+ if (!state)
9865
+ return state;
9866
+ const owner = state.metadata?.[BUDGET_OWNER_NAMESPACE_KEY];
9867
+ if (state.scope?.namespace !== bound.namespace || owner !== ownerNamespace) {
9868
+ throw new HarnessStateConflictError("Budget ledger is not bound to this namespace.");
9869
+ }
9870
+ return state;
9871
+ };
9872
+ return {
9873
+ ...primary,
9874
+ load: async (id, requestedScope) => {
9875
+ if (!isSdkBudget(requestedScope) && !isBoundBudget(requestedScope))
9876
+ return primary.load(id, requestedScope);
9877
+ assertId(id);
9878
+ return assertStored(await budget.load(id, bound));
9879
+ },
9880
+ save: async (state, options) => {
9881
+ if (!isSdkBudget(state.scope) && !isBoundBudget(state.scope))
9882
+ return primary.save(state, options);
9883
+ assertId(state.runId);
9884
+ if (state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true) {
9885
+ throw new HarnessStateConflictError("Only SDK budget ledger states may use the reserved namespace.");
9886
+ }
9887
+ return budget.save(bindState(state), options);
9888
+ }
9889
+ };
9890
+ };
9891
+
9892
+ // src/runtime/task-budget.ts
9721
9893
  import {
9722
9894
  createAgentBudgetCoordinator,
9723
9895
  fingerprintAgentHarness as fingerprintAgentHarness2,
@@ -9836,10 +10008,10 @@ class TaskBudget {
9836
10008
  this.accountRunId = TaskBudget.accountId(scope, taskId);
9837
10009
  this.policy = structuredClone(account.policy);
9838
10010
  const budgetId = this.accountRunId;
9839
- this.coordinator = createAgentBudgetCoordinator({ store, scope, budgetId, limits: this.policy.limits });
10011
+ this.coordinator = createAgentBudgetCoordinator({ store: bindBudgetLedgerStore(store, scope), scope, budgetId, limits: this.policy.limits });
9840
10012
  this.coordinatorId = this.coordinator.id;
9841
10013
  this.budgetRunId = `budget_${fingerprintAgentHarness2({ budgetId, scope }).slice("sha256:".length)}`;
9842
- this.budgetScope = { tenantId: scope.tenantId, ...scope.userId ? { userId: scope.userId } : {}, namespace: "__zhivex_budget__" };
10014
+ this.budgetScope = budgetLedgerScope(scope);
9843
10015
  }
9844
10016
  static accountId(scope, taskId) {
9845
10017
  return `task_budget_${fingerprintAgentHarness2({ scope, taskId }).slice("sha256:".length)}`;
@@ -10354,37 +10526,7 @@ class TaskBudget {
10354
10526
  }
10355
10527
 
10356
10528
  // src/persistence/operations.ts
10357
- var budgetScopeFor = (config) => ({
10358
- tenantId: config.scope.tenantId,
10359
- ...config.scope.userId ? { userId: config.scope.userId } : {},
10360
- namespace: "__zhivex_budget__"
10361
- });
10362
- var withBudgetLedgerStore = (primary, budget, config) => {
10363
- const scope = budgetScopeFor(config);
10364
- const budgetScope = (value) => value?.namespace === scope.namespace && value.tenantId === scope.tenantId && (value.userId ?? undefined) === (scope.userId ?? undefined);
10365
- const assertId = (id) => {
10366
- if (!/^budget_[a-f0-9]{64}$/.test(id))
10367
- throw new HarnessStateConflictError("Invalid budget ledger identity.");
10368
- };
10369
- return {
10370
- ...primary,
10371
- load: async (id, requestedScope) => {
10372
- if (!budgetScope(requestedScope))
10373
- return primary.load(id, requestedScope);
10374
- assertId(id);
10375
- return budget.load(id, requestedScope);
10376
- },
10377
- save: async (state, options) => {
10378
- if (!budgetScope(state.scope))
10379
- return primary.save(state, options);
10380
- assertId(state.runId);
10381
- if (state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true) {
10382
- throw new HarnessStateConflictError("Only SDK budget ledger states may use the reserved namespace.");
10383
- }
10384
- return budget.save(state, options);
10385
- }
10386
- };
10387
- };
10529
+ var withBudgetLedgerStore = (primary, budget, config) => bindBudgetLedgerStore(primary, config.scope, budget);
10388
10530
  var HARNESS_SQLITE_FILE = "operations.sqlite";
10389
10531
  var HARNESS_OPERATIONS_SCHEMA_VERSION = 1;
10390
10532
  var runSummary = (state) => {
@@ -10555,13 +10697,12 @@ var migrateLegacyFileRuns = async (directory, target, config) => {
10555
10697
  return result;
10556
10698
  };
10557
10699
  var openHarnessPersistence = async (config, options = {}) => {
10558
- if (config.scope.namespace === "__zhivex_budget__")
10559
- throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
10700
+ const budgetScope = budgetLedgerScope(config.scope);
10560
10701
  await validateStateDirectory(config.workspace, config.stateDirectory);
10561
10702
  await ensurePrivateStateDirectory(config.stateDirectory);
10562
10703
  await protectStateFromGit(config.workspace, config.stateDirectory);
10563
10704
  if (config.storeBackend === "file") {
10564
- const store = withBudgetLedgerStore(createFileAgentRunStore({ directory: config.stateDirectory, scope: config.scope }), createFileAgentRunStore({ directory: config.stateDirectory, scope: budgetScopeFor(config) }), config);
10705
+ const store = withBudgetLedgerStore(createFileAgentRunStore({ directory: config.stateDirectory, scope: config.scope }), createFileAgentRunStore({ directory: config.stateDirectory, scope: budgetScope }), config);
10565
10706
  const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
10566
10707
  return {
10567
10708
  backend: "file",
@@ -10606,7 +10747,7 @@ var openHarnessPersistence = async (config, options = {}) => {
10606
10747
  database.exec("PRAGMA foreign_keys = ON");
10607
10748
  await chmod3(databasePath, 384);
10608
10749
  const databaseLike = sqliteAdapter(database);
10609
- const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
10750
+ const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScope }), config);
10610
10751
  database.exec(`CREATE TRIGGER IF NOT EXISTS zhivex_task_budget_retain_run
10611
10752
  BEFORE DELETE ON zhivex_agent_runs
10612
10753
  WHEN json_type(OLD.state_json, '$.metadata.zhivexTaskBudgetAccountV1') IS NOT NULL
@@ -10774,7 +10915,7 @@ var projectMemoryBinding = (workspace, scope) => {
10774
10915
  segment(scope.userId);
10775
10916
  if (scope.namespace !== undefined)
10776
10917
  segment(scope.namespace);
10777
- if (scope.namespace === "__zhivex_budget__")
10918
+ if (isReservedBudgetNamespace(scope.namespace))
10778
10919
  throw new HarnessConfigError("Reserved memory namespace.");
10779
10920
  const key = (kind, value) => createHash15("sha256").update(kind).update("\x00").update(value).digest("hex");
10780
10921
  const workspaceKey = key("workspace", workspace);
@@ -11166,7 +11307,8 @@ var coordinatorIds = (runs) => new Set(runs.flatMap(({ state }) => {
11166
11307
  var validateBudgetLedger = (config, run, identities) => {
11167
11308
  const state = validatedRunState(run.state, `Budget ledger ${run.key}`);
11168
11309
  const identity = state.metadata?.budgetIdentity;
11169
- if (state.scope?.namespace !== "__zhivex_budget__" || state.scope?.tenantId !== config.scope.tenantId || (state.scope?.userId ?? undefined) !== (config.scope.userId ?? undefined) || state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true || typeof identity !== "string" || !identities.has(identity) || !/^budget_[a-f0-9]{64}$/.test(state.runId) || !sdkRunKeyMatches(run.key, state.scope, state.runId)) {
11310
+ const ownerNamespace = config.scope.namespace;
11311
+ if (!ownerNamespace || state.scope?.namespace !== budgetLedgerNamespace(ownerNamespace) || state.metadata?.[BUDGET_OWNER_NAMESPACE_KEY] !== ownerNamespace || state.scope?.tenantId !== config.scope.tenantId || (state.scope?.userId ?? undefined) !== (config.scope.userId ?? undefined) || state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true || typeof identity !== "string" || !identities.has(identity) || !/^budget_[a-f0-9]{64}$/.test(state.runId) || !sdkRunKeyMatches(run.key, state.scope, state.runId)) {
11170
11312
  throw new HarnessStateConflictError("Budget ledger is not bound to an exported run and durable scope.");
11171
11313
  }
11172
11314
  assertTerminalState(state, `Budget ledger ${run.key}`);
@@ -11300,7 +11442,9 @@ var readPayload = async (config, recordedWorkspace, accessLease) => {
11300
11442
  const metadata = state.metadata;
11301
11443
  if (typeof metadata?.budgetIdentity !== "string" || !identities.has(metadata.budgetIdentity)) {
11302
11444
  const scope = state.scope;
11303
- if (scope?.namespace === "__zhivex_budget__" && scope.tenantId === config.scope.tenantId && (scope.userId ?? undefined) === (config.scope.userId ?? undefined)) {
11445
+ const sameTenant = scope?.tenantId === config.scope.tenantId && (scope?.userId ?? undefined) === (config.scope.userId ?? undefined);
11446
+ const boundNamespace = config.scope.namespace ? budgetLedgerNamespace(config.scope.namespace) : undefined;
11447
+ if (sameTenant && (scope?.namespace === BUDGET_LEDGER_NAMESPACE || scope?.namespace === boundNamespace)) {
11304
11448
  throw new HarnessStateConflictError("An unlinked shared budget ledger cannot be safely assigned to this backup scope; retain the original database.");
11305
11449
  }
11306
11450
  return [];
@@ -13454,6 +13598,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
13454
13598
  var LOCAL_TOOLS = LOCAL_TOOL_NAMES;
13455
13599
  var MAX_USER_STEERING = 10;
13456
13600
  var CONTEXT_PRIORITY = "Latest user > chronological user steering > historical objective. Assistant never overrides user facts. Questions retain task.";
13601
+ var EARLIER = "[before later user messages] ";
13457
13602
  var summarizeHarnessMessages = (messages, maxCharacters = 4000) => {
13458
13603
  const redaction2 = createRedactionPolicy8({ includeEmails: true });
13459
13604
  const clean = (text3, limit = 512) => redaction2.redactText(text3).replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]").replace(/\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)|api[_-]?key|access[_-]?token|password)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "$1$2[REDACTED]").replace(/\b(?:sk|ghp|gho|github_pat)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]").replace(/\s+/g, " ").trim().slice(0, limit);
@@ -13479,6 +13624,12 @@ var summarizeHarnessMessages = (messages, maxCharacters = 4000) => {
13479
13624
  omitted = true;
13480
13625
  }
13481
13626
  };
13627
+ const markRecentEarlier = () => {
13628
+ const marked = recent.map((value) => value.startsWith(EARLIER) ? value : `${EARLIER}${value}`);
13629
+ recent.length = 0;
13630
+ for (const value of marked)
13631
+ add(recent, value, 4);
13632
+ };
13482
13633
  const rememberObservation = (tool3, detail) => {
13483
13634
  if (typeof tool3 !== "string" || !LOCAL_TOOLS.has(tool3) || typeof detail !== "string")
13484
13635
  return;
@@ -13596,6 +13747,7 @@ var summarizeHarnessMessages = (messages, maxCharacters = 4000) => {
13596
13747
  objective = text3;
13597
13748
  if (historicalObjective)
13598
13749
  add(steering, text3, MAX_USER_STEERING);
13750
+ markRecentEarlier();
13599
13751
  } else
13600
13752
  add(recent, `${message.role}: ${text3}`, 4);
13601
13753
  } else if (part.type === "tool-call") {
@@ -15140,7 +15292,8 @@ var resolvers = {
15140
15292
  const parsed = moveFileInputSchema.parse(input);
15141
15293
  return [parsed.source, parsed.destination];
15142
15294
  },
15143
- quarantine_file: (input) => [quarantineFileInputSchema.parse(input).path]
15295
+ quarantine_file: (input) => [quarantineFileInputSchema.parse(input).path],
15296
+ restore_file: (input) => [restoreFileInputSchema.parse(input).destination]
15144
15297
  };
15145
15298
  var validateBuiltinToolPolicy = (policy, tools) => {
15146
15299
  for (const rule of policy.rules)
@@ -15274,7 +15427,7 @@ import { z as z34 } from "zod";
15274
15427
  // package.json
15275
15428
  var package_default = {
15276
15429
  name: "@zhivex-ai/harness",
15277
- version: "1.4.1",
15430
+ version: "1.6.2",
15278
15431
  description: "Governed, provider-portable runtime for coding agents with durable approvals and isolated repository execution.",
15279
15432
  type: "module",
15280
15433
  license: "MIT",
@@ -15374,6 +15527,7 @@ var package_default = {
15374
15527
  "LICENSE",
15375
15528
  "docs/CHANGE_ENVELOPES.md",
15376
15529
  "docs/CLI.md",
15530
+ "docs/AUTO_APPROVE.md",
15377
15531
  "docs/WEB_LIMITS.md",
15378
15532
  "docs/CODE_RELEASE.md",
15379
15533
  "docs/CODE_WEB_TASKS.md",
@@ -15490,26 +15644,26 @@ var package_default = {
15490
15644
  "smoke:mcp:installed": "bun run build && bun scripts/mcp-installed-smoke.ts"
15491
15645
  },
15492
15646
  dependencies: {
15493
- "@zhivex-ai/agents": "1.11.0",
15494
- "@zhivex-ai/anthropic": "0.14.0",
15495
- "@zhivex-ai/azure-openai": "0.7.8",
15496
- "@zhivex-ai/bedrock": "2.0.7",
15497
- "@zhivex-ai/core": "1.34.0",
15498
- "@zhivex-ai/gemini": "0.13.0",
15499
- "@zhivex-ai/meta": "0.2.9",
15500
- "@zhivex-ai/openai": "0.16.0",
15501
- "@zhivex-ai/qwen": "0.16.4",
15502
- "@zhivex-ai/vertex": "1.2.7",
15503
- "google-auth-library": "11.1.0",
15504
- zod: "4.6.5"
15647
+ "@zhivex-ai/agents": "1.12.0",
15648
+ "@zhivex-ai/anthropic": "0.15.0",
15649
+ "@zhivex-ai/azure-openai": "0.8.0",
15650
+ "@zhivex-ai/bedrock": "2.1.0",
15651
+ "@zhivex-ai/core": "1.36.0",
15652
+ "@zhivex-ai/gemini": "0.14.0",
15653
+ "@zhivex-ai/meta": "0.3.0",
15654
+ "@zhivex-ai/openai": "0.17.0",
15655
+ "@zhivex-ai/qwen": "0.18.0",
15656
+ "@zhivex-ai/vertex": "1.3.0",
15657
+ "google-auth-library": "11.3.1",
15658
+ zod: "4.6.7"
15505
15659
  },
15506
15660
  overrides: {
15507
- "@zhivex-ai/core": "1.34.0"
15661
+ "@zhivex-ai/core": "1.36.0"
15508
15662
  },
15509
15663
  devDependencies: {
15510
15664
  "@agentclientprotocol/sdk": "1.8.0",
15511
15665
  "@modelcontextprotocol/server": "2.3.1",
15512
- "@types/bun": "1.4.2",
15666
+ "@types/bun": "1.4.3",
15513
15667
  "@types/node": "^26.6.5",
15514
15668
  typescript: "7.0.2",
15515
15669
  "typescript-compiler-api": "npm:typescript@6.0.3"
@@ -16799,7 +16953,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
16799
16953
  }),
16800
16954
  restore_file: tool5({
16801
16955
  name: "restore_file",
16802
- description: "Restore a quarantined file to its original path or an explicit safe destination without overwriting content unexpectedly.",
16956
+ description: "Restore a quarantined file to the required destination after the reviewed expectedDigest matches the quarantined bytes. Restore does not fall back to the original path and never overwrites an existing file.",
16803
16957
  schema: restoreFileInputSchema,
16804
16958
  ...mutationApproval,
16805
16959
  execute: async (input, context) => serializeJsonValue9(editContractDocument("restore-result", await (harnessExecutionSession(context)?.workspace ?? workspace).restoreQuarantined(input)))
@@ -70,8 +70,8 @@ export declare const quarantineFileInputSchema: z.ZodObject<{
70
70
  export type QuarantineFileInput = z.infer<typeof quarantineFileInputSchema>;
71
71
  export declare const restoreFileInputSchema: z.ZodObject<{
72
72
  quarantineId: z.ZodString;
73
- destination: z.ZodOptional<z.ZodString>;
74
- expectedDigest: z.ZodOptional<z.ZodString>;
73
+ destination: z.ZodString;
74
+ expectedDigest: z.ZodString;
75
75
  }, z.core.$strict>;
76
76
  export type RestoreFileInput = z.infer<typeof restoreFileInputSchema>;
77
77
  export declare const mutationOperationSchema: z.ZodEnum<{
@@ -1,4 +1,5 @@
1
- /** Shared lexical exclusions for workspace access and acceptance preflight. */
1
+ /** Shared lexical exclusions for workspace access and acceptance preflight.
2
+ * Comparisons use toLowerCase so a Turkish locale cannot map ASCII I to dotless ı and skip .git or id_rsa. */
2
3
  export declare const HARD_IGNORES: Set<string>;
3
4
  export declare const isSensitiveName: (name: string) => boolean;
4
5
  export declare const isHardIgnored: (relativePath: string) => boolean;
@@ -0,0 +1,19 @@
1
+ export interface ControlHit {
2
+ line: number;
3
+ label: string;
4
+ }
5
+ /** C0 controls other than tab, LF, and CR, plus DEL and Unicode C1 controls. */
6
+ export declare const firstDisallowedControl: (text: string) => ControlHit | undefined;
7
+ /** Raw C0/DEL bytes. UTF-8 continuation bytes are left alone. */
8
+ export declare const firstDisallowedControlBytes: (bytes: Uint8Array) => ControlHit | undefined;
9
+ export declare const refuseControlMessage: (hit: ControlHit) => string;
10
+ export declare const binaryAfterEditMessage: (hit: ControlHit) => string;
11
+ /**
12
+ * Text edits refuse NUL and other control bytes unless the host passes
13
+ * allowControlBytes. That switch is not part of the model tool schema.
14
+ * afterWrite is the publication check; tests replace it to prove the rollback path.
15
+ */
16
+ export declare const textGuards: {
17
+ beforeWrite(content: string, allowControlBytes: boolean): void;
18
+ afterWrite(bytes: Uint8Array, allowControlBytes: boolean): ControlHit | undefined;
19
+ };
@@ -1,5 +1,9 @@
1
1
  import { type ReplacementEdit } from "./replacement-edits.js";
2
2
  import { type ApplyEditProposalInput, type ApplyPatchResult, type FileDigest, type MoveFileInput, type MoveFileResult, type MutationAuditEntry, type QuarantineFileInput, type QuarantineFileResult, type RestoreFileInput, type RestoreFileResult } from "./edit-contracts.js";
3
+ /** Host-only. Model tool input cannot set this, and the default is off. */
4
+ interface HostTextEditOptions {
5
+ allowControlBytes?: boolean;
6
+ }
3
7
  export type HarnessCheck = string;
4
8
  export interface WorkspaceFile {
5
9
  path: string;
@@ -188,12 +192,12 @@ export declare class Workspace {
188
192
  }[];
189
193
  }>;
190
194
  private replacementChanges;
191
- applyReplacement(input: ReplacementEdit): Promise<ApplyPatchResult>;
192
- applyPatch(input: ApplyEditProposalInput): Promise<ApplyPatchResult>;
195
+ applyReplacement(input: ReplacementEdit, host?: HostTextEditOptions): Promise<ApplyPatchResult>;
196
+ applyPatch(input: ApplyEditProposalInput, host?: HostTextEditOptions): Promise<ApplyPatchResult>;
193
197
  applyPatchWithModes(input: ApplyEditProposalInput, modes: ReadonlyMap<string, {
194
198
  beforeMode?: number;
195
199
  afterMode: number;
196
- }>, assertActive?: () => Promise<void>): Promise<ApplyPatchResult>;
200
+ }>, assertActive?: () => Promise<void>, host?: HostTextEditOptions): Promise<ApplyPatchResult>;
197
201
  private applyPatchWithModesLocked;
198
202
  /** @deprecated Use applyPatch with an inspected expectedDigest. */
199
203
  writeFile(relativePath: string, content: string, overwrite?: boolean): Promise<{
@@ -214,6 +218,19 @@ export declare class Workspace {
214
218
  private readInternalRegularFile;
215
219
  quarantineFile(input: QuarantineFileInput, assertActive?: () => Promise<void>): Promise<QuarantineFileResult>;
216
220
  private quarantineFileLocked;
221
+ private readRestorableQuarantine;
222
+ /** Read-only preview of a pinned restore. Destination and digest are required, and nothing is written. */
223
+ previewRestore(input: unknown): Promise<{
224
+ proposalId: string;
225
+ files: {
226
+ path: string;
227
+ expectedDigest: string;
228
+ before: null;
229
+ after: string;
230
+ afterDigest: string;
231
+ operation: "restore";
232
+ }[];
233
+ }>;
217
234
  restoreQuarantined(input: RestoreFileInput): Promise<RestoreFileResult>;
218
235
  private restoreQuarantinedLocked;
219
236
  runCheck(check: HarnessCheck, expectedScript: string, allowedChecks?: readonly string[]): Promise<CommandResult>;
@@ -223,3 +240,4 @@ export declare class Workspace {
223
240
  staged: CommandResult;
224
241
  }>;
225
242
  }
243
+ export {};
package/dist/zhx.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  HARNESS_VERSION,
4
4
  NODE_ENGINE_RANGE,
5
5
  shortCliHelp
6
- } from "./cli-entry-3s63zn4w.js";
6
+ } from "./cli-entry-10th5awp.js";
7
7
  import {
8
8
  __require
9
9
  } from "./cli-entry-1bx83sw4.js";
@@ -41,6 +41,6 @@ if (argv.length === 1 && ["--version", "-v", "version"].includes(argv[0])) {
41
41
  process.stdout.write(`${shortCliHelp(HARNESS_VERSION)}
42
42
  `);
43
43
  } else {
44
- const { runCli } = await import("./cli-1cf4dcnj.js");
44
+ const { runCli } = await import("./cli-84en174d.js");
45
45
  await runCli(argv);
46
46
  }