@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
@@ -3816,8 +3816,8 @@ var quarantineFileInputSchema = z6.strictObject({
3816
3816
  });
3817
3817
  var restoreFileInputSchema = z6.strictObject({
3818
3818
  quarantineId: z6.string().min(1).max(200).regex(/^[A-Za-z0-9._:-]+$/),
3819
- destination: workspaceFilePathSchema.optional(),
3820
- expectedDigest: fileDigestSchema.optional()
3819
+ destination: workspaceFilePathSchema.describe("Required workspace path for the quarantined bytes. Restore does not fall back to the original path."),
3820
+ expectedDigest: fileDigestSchema.describe("Required sha256 of the quarantined bytes the operator reviewed.")
3821
3821
  });
3822
3822
  var mutationOperationSchema = z6.enum(["create", "update", "move", "quarantine", "restore"]);
3823
3823
  var mutationAuditEntrySchema = z6.strictObject({
@@ -4154,13 +4154,62 @@ var runPortableProcess = async (command, options = {}) => {
4154
4154
  }
4155
4155
  };
4156
4156
 
4157
+ // src/workspace/text-integrity.ts
4158
+ var controlLabel = (code) => code === 0 ? "NUL byte" : `U+${code.toString(16).padStart(4, "0")}`;
4159
+ var firstDisallowedControl = (text2) => {
4160
+ let line = 1;
4161
+ for (let index = 0;index < text2.length; index += 1) {
4162
+ const code = text2.charCodeAt(index);
4163
+ if (code === 10) {
4164
+ line += 1;
4165
+ continue;
4166
+ }
4167
+ if (code === 9 || code === 13)
4168
+ continue;
4169
+ if (code < 32 || code === 127 || code >= 128 && code <= 159)
4170
+ return { line, label: controlLabel(code) };
4171
+ }
4172
+ return;
4173
+ };
4174
+ var firstDisallowedControlBytes = (bytes) => {
4175
+ let line = 1;
4176
+ for (let index = 0;index < bytes.length; index += 1) {
4177
+ const code = bytes[index] ?? 0;
4178
+ if (code === 10) {
4179
+ line += 1;
4180
+ continue;
4181
+ }
4182
+ if (code === 9 || code === 13)
4183
+ continue;
4184
+ if (code < 32 || code === 127)
4185
+ return { line, label: controlLabel(code) };
4186
+ }
4187
+ return;
4188
+ };
4189
+ 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.`;
4190
+ var binaryAfterEditMessage = (hit) => `file is binary after edit: ${hit.label} at L${hit.line}. The edit was reverted. Read the file again before retrying.`;
4191
+ var textGuards = {
4192
+ beforeWrite(content, allowControlBytes) {
4193
+ if (allowControlBytes)
4194
+ return;
4195
+ const problem = firstDisallowedControl(content);
4196
+ if (problem)
4197
+ throw new HarnessWorkspaceError(refuseControlMessage(problem));
4198
+ },
4199
+ afterWrite(bytes, allowControlBytes) {
4200
+ if (allowControlBytes)
4201
+ return;
4202
+ return firstDisallowedControlBytes(bytes);
4203
+ }
4204
+ };
4205
+
4157
4206
  // src/workspace/path-policy.ts
4158
4207
  var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
4159
4208
  var isSensitiveName = (name) => {
4160
- const normalized = name.toLocaleLowerCase();
4209
+ const normalized = name.toLowerCase();
4161
4210
  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));
4162
4211
  };
4163
- var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
4212
+ var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
4164
4213
 
4165
4214
  // src/workspace/workspace.ts
4166
4215
  var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
@@ -4440,7 +4489,7 @@ class Workspace {
4440
4489
  throw new HarnessWorkspaceError(`The path escapes the workspace: ${relativePath}`);
4441
4490
  const relative = wirePath(path8.relative(this.root, candidate));
4442
4491
  if (relative && isHardIgnored(relative)) {
4443
- const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
4492
+ const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
4444
4493
  throw new HarnessWorkspaceError(`The path is protected by the harness policy: ${blocked}`);
4445
4494
  }
4446
4495
  return candidate;
@@ -4483,8 +4532,11 @@ class Workspace {
4483
4532
  maxBytes: MAX_FILE_BYTES
4484
4533
  });
4485
4534
  const { contents } = file;
4486
- if (!allowBinary && contents.includes(0))
4487
- throw new HarnessWorkspaceError("The file appears to be binary and cannot be read as text.");
4535
+ if (!allowBinary && contents.includes(0)) {
4536
+ const hit = firstDisallowedControlBytes(contents);
4537
+ const where = hit ? `: ${hit.label} at L${hit.line}` : "";
4538
+ throw new HarnessWorkspaceError(`The file appears to be binary and cannot be read as text${where}.`);
4539
+ }
4488
4540
  return {
4489
4541
  path: wirePath(path8.relative(this.root, safe.path)),
4490
4542
  absolutePath: safe.path,
@@ -5151,19 +5203,23 @@ class Workspace {
5151
5203
  if (position < 0 || text2.indexOf(edit.oldText, position + 1) >= 0) {
5152
5204
  throw new HarnessWorkspaceError("Replacement requires exactly one literal match; include more surrounding context.");
5153
5205
  }
5154
- return [{ path: edit.path, expectedDigest: edit.expectedDigest, content: text2.slice(0, position) + edit.newText + text2.slice(position + edit.oldText.length) }];
5206
+ return [{
5207
+ path: edit.path,
5208
+ expectedDigest: edit.expectedDigest,
5209
+ content: text2.slice(0, position) + edit.newText + text2.slice(position + edit.oldText.length)
5210
+ }];
5155
5211
  }
5156
- async applyReplacement(input) {
5212
+ async applyReplacement(input, host = {}) {
5157
5213
  const changes = await this.replacementChanges(input);
5158
- return this.applyPatch({ proposalId: createEditProposal({ changes }).proposalId, changes });
5214
+ return this.applyPatch({ proposalId: createEditProposal({ changes }).proposalId, changes }, host);
5159
5215
  }
5160
- async applyPatch(input) {
5161
- return this.applyPatchWithModes(input, new Map);
5216
+ async applyPatch(input, host = {}) {
5217
+ return this.applyPatchWithModes(input, new Map, undefined, host);
5162
5218
  }
5163
- async applyPatchWithModes(input, modes, assertActive) {
5164
- return withWorkspaceMutation(this.root, () => this.applyPatchWithModesLocked(input, modes, assertActive));
5219
+ async applyPatchWithModes(input, modes, assertActive, host = {}) {
5220
+ return withWorkspaceMutation(this.root, () => this.applyPatchWithModesLocked(input, modes, assertActive, host));
5165
5221
  }
5166
- async applyPatchWithModesLocked(input, modes, assertActive) {
5222
+ async applyPatchWithModesLocked(input, modes, assertActive, host = {}) {
5167
5223
  const proposal = validateEditProposal(input);
5168
5224
  this.checkAuditCapacity(proposal.changes.length);
5169
5225
  const targetPaths = new Set(proposal.changes.map((change) => change.path));
@@ -5208,6 +5264,7 @@ class Workspace {
5208
5264
  throw new HarnessWorkspaceError(`Stale patch mode rejected for ${change.path}: expected ${modeBinding.beforeMode.toString(8)}, found ${before.mode.toString(8)}.`);
5209
5265
  }
5210
5266
  }
5267
+ textGuards.beforeWrite(change.content, host.allowControlBytes === true);
5211
5268
  const temporary = await this.stageFile(safe.path, change.content, modeBinding?.afterMode ?? before?.mode ?? 420);
5212
5269
  prepared.push({
5213
5270
  change,
@@ -5247,6 +5304,13 @@ class Workspace {
5247
5304
  await unlink(item.temporary);
5248
5305
  }
5249
5306
  committed.push(item);
5307
+ const published = await readRegularFileNoFollow(item.target, {
5308
+ label: "The published file",
5309
+ maxBytes: MAX_FILE_BYTES
5310
+ });
5311
+ const corrupt = textGuards.afterWrite(published.contents, host.allowControlBytes === true);
5312
+ if (corrupt)
5313
+ throw new HarnessWorkspaceError(binaryAfterEditMessage(corrupt));
5250
5314
  await assertActive?.();
5251
5315
  }
5252
5316
  } catch (error) {
@@ -5423,30 +5487,65 @@ class Workspace {
5423
5487
  const audit = await this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
5424
5488
  return { quarantineId, path: parsed.path, digest: source.digest, audit };
5425
5489
  }
5426
- async restoreQuarantined(input) {
5427
- return withWorkspaceMutation(this.root, () => this.restoreQuarantinedLocked(input));
5428
- }
5429
- async restoreQuarantinedLocked(input) {
5430
- const parsed = restoreFileInputSchema.parse(input);
5431
- this.checkAuditCapacity();
5490
+ async readRestorableQuarantine(quarantineId) {
5432
5491
  const directory = await this.secureQuarantineDirectory(false);
5433
5492
  let manifest;
5434
5493
  try {
5435
- const manifestFile = await this.readInternalRegularFile(path8.join(directory, `${parsed.quarantineId}.json`), "Quarantine manifest", 64 * 1024);
5494
+ const manifestFile = await this.readInternalRegularFile(path8.join(directory, `${quarantineId}.json`), "Quarantine manifest", 64 * 1024);
5436
5495
  manifest = JSON.parse(manifestFile.contents.toString("utf8"));
5437
5496
  } catch {
5438
- throw new HarnessWorkspaceError(`Quarantine entry was not found: ${parsed.quarantineId}.`);
5497
+ throw new HarnessWorkspaceError(`Quarantine entry was not found: ${quarantineId}.`);
5439
5498
  }
5440
- if (manifest.schemaVersion !== 1 || manifest.quarantineId !== parsed.quarantineId || manifest.status !== "quarantined") {
5441
- throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${parsed.quarantineId}.`);
5499
+ if (manifest.schemaVersion !== 1 || manifest.quarantineId !== quarantineId || manifest.status !== "quarantined") {
5500
+ throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${quarantineId}.`);
5442
5501
  }
5443
- const dataPath = path8.join(directory, `${parsed.quarantineId}.data`);
5502
+ const dataPath = path8.join(directory, `${quarantineId}.data`);
5444
5503
  const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
5445
5504
  const digest2 = digestBytes(dataFile.contents);
5446
- if (digest2 !== manifest.digest || parsed.expectedDigest && parsed.expectedDigest !== digest2) {
5505
+ if (digest2 !== manifest.digest) {
5506
+ throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${quarantineId}.`);
5507
+ }
5508
+ return { manifest, dataPath, dataFile, digest: digest2 };
5509
+ }
5510
+ async previewRestore(input) {
5511
+ const parsed = restoreFileInputSchema.parse(input);
5512
+ const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
5513
+ if (parsed.expectedDigest !== quarantined.digest) {
5447
5514
  throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
5448
5515
  }
5449
- const destinationPath = parsed.destination ?? manifest.originalPath;
5516
+ const destination = await this.safePath(parsed.destination, { allowMissing: true });
5517
+ if (destination.exists)
5518
+ throw new HarnessWorkspaceError(`Restore destination already exists: ${parsed.destination}.`);
5519
+ let after;
5520
+ try {
5521
+ after = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(quarantined.dataFile.contents);
5522
+ } catch {
5523
+ throw new HarnessWorkspaceError("Preview requires valid UTF-8 text.");
5524
+ }
5525
+ return {
5526
+ proposalId: parsed.expectedDigest,
5527
+ files: [{
5528
+ path: parsed.destination,
5529
+ expectedDigest: parsed.expectedDigest,
5530
+ before: null,
5531
+ after,
5532
+ afterDigest: quarantined.digest,
5533
+ operation: "restore"
5534
+ }]
5535
+ };
5536
+ }
5537
+ async restoreQuarantined(input) {
5538
+ return withWorkspaceMutation(this.root, () => this.restoreQuarantinedLocked(input));
5539
+ }
5540
+ async restoreQuarantinedLocked(input) {
5541
+ const parsed = restoreFileInputSchema.parse(input);
5542
+ this.checkAuditCapacity();
5543
+ const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
5544
+ if (parsed.expectedDigest !== quarantined.digest) {
5545
+ throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
5546
+ }
5547
+ const { manifest, dataPath, dataFile, digest: digest2 } = quarantined;
5548
+ const destinationPath = parsed.destination;
5450
5549
  const destination = await this.safePath(destinationPath, { allowMissing: true });
5451
5550
  if (destination.exists)
5452
5551
  throw new HarnessWorkspaceError(`Restore destination already exists: ${destinationPath}.`);
@@ -6671,7 +6770,7 @@ var importPatch = async (runId, expectedPatchId, host, base, current, maxFileWri
6671
6770
  const rollbackErrors = [];
6672
6771
  for (const item of [...quarantined].reverse()) {
6673
6772
  try {
6674
- await host.restoreQuarantined({ quarantineId: item.quarantineId, expectedDigest: item.digest });
6773
+ await host.restoreQuarantined({ quarantineId: item.quarantineId, destination: item.path, expectedDigest: item.digest });
6675
6774
  } catch (rollbackError) {
6676
6775
  rollbackErrors.push(rollbackError);
6677
6776
  }
@@ -7342,6 +7441,79 @@ var taskUsageAdmission = new AsyncLocalStorage3;
7342
7441
  import { AsyncLocalStorage as AsyncLocalStorage5 } from "node:async_hooks";
7343
7442
  import { randomUUID as randomUUID5 } from "node:crypto";
7344
7443
  import { z as z12 } from "zod";
7444
+
7445
+ // src/runtime/budget-ledger-scope.ts
7446
+ var BUDGET_LEDGER_NAMESPACE = "__zhivex_budget__";
7447
+ var BUDGET_OWNER_NAMESPACE_KEY = "budgetOwnerNamespace";
7448
+ var isReservedBudgetNamespace = (namespace) => namespace === BUDGET_LEDGER_NAMESPACE || (namespace?.startsWith(`${BUDGET_LEDGER_NAMESPACE}:`) ?? false);
7449
+ var budgetLedgerNamespace = (namespace) => {
7450
+ if (!namespace || isReservedBudgetNamespace(namespace)) {
7451
+ throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
7452
+ }
7453
+ return `${BUDGET_LEDGER_NAMESPACE}:${namespace}`;
7454
+ };
7455
+ var budgetLedgerScope = (scope) => {
7456
+ if (scope.namespace && isReservedBudgetNamespace(scope.namespace)) {
7457
+ throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
7458
+ }
7459
+ return {
7460
+ tenantId: scope.tenantId,
7461
+ ...scope.userId ? { userId: scope.userId } : {},
7462
+ namespace: scope.namespace ? budgetLedgerNamespace(scope.namespace) : `${BUDGET_LEDGER_NAMESPACE}:`
7463
+ };
7464
+ };
7465
+ var sameIdentity = (left, right) => left?.tenantId === right.tenantId && (left.userId ?? undefined) === (right.userId ?? undefined);
7466
+ var bindBudgetLedgerStore = (primary, scope, budget = primary) => {
7467
+ const bound = budgetLedgerScope(scope);
7468
+ const ownerNamespace = scope.namespace ?? "";
7469
+ const isSdkBudget = (value) => sameIdentity(value, bound) && value?.namespace === BUDGET_LEDGER_NAMESPACE;
7470
+ const isBoundBudget = (value) => sameIdentity(value, bound) && value?.namespace === bound.namespace;
7471
+ const assertId = (id) => {
7472
+ if (!/^budget_[a-f0-9]{64}$/.test(id))
7473
+ throw new HarnessStateConflictError("Invalid budget ledger identity.");
7474
+ };
7475
+ const bindState = (state) => {
7476
+ const metadata = { ...state.metadata ?? {} };
7477
+ const owner = metadata[BUDGET_OWNER_NAMESPACE_KEY];
7478
+ if (owner !== undefined && owner !== ownerNamespace) {
7479
+ throw new HarnessStateConflictError("Budget ledger is bound to another namespace.");
7480
+ }
7481
+ return {
7482
+ ...state,
7483
+ scope: bound,
7484
+ metadata: { ...metadata, [BUDGET_OWNER_NAMESPACE_KEY]: ownerNamespace }
7485
+ };
7486
+ };
7487
+ const assertStored = (state) => {
7488
+ if (!state)
7489
+ return state;
7490
+ const owner = state.metadata?.[BUDGET_OWNER_NAMESPACE_KEY];
7491
+ if (state.scope?.namespace !== bound.namespace || owner !== ownerNamespace) {
7492
+ throw new HarnessStateConflictError("Budget ledger is not bound to this namespace.");
7493
+ }
7494
+ return state;
7495
+ };
7496
+ return {
7497
+ ...primary,
7498
+ load: async (id, requestedScope) => {
7499
+ if (!isSdkBudget(requestedScope) && !isBoundBudget(requestedScope))
7500
+ return primary.load(id, requestedScope);
7501
+ assertId(id);
7502
+ return assertStored(await budget.load(id, bound));
7503
+ },
7504
+ save: async (state, options) => {
7505
+ if (!isSdkBudget(state.scope) && !isBoundBudget(state.scope))
7506
+ return primary.save(state, options);
7507
+ assertId(state.runId);
7508
+ if (state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true) {
7509
+ throw new HarnessStateConflictError("Only SDK budget ledger states may use the reserved namespace.");
7510
+ }
7511
+ return budget.save(bindState(state), options);
7512
+ }
7513
+ };
7514
+ };
7515
+
7516
+ // src/runtime/task-budget.ts
7345
7517
  import {
7346
7518
  createAgentBudgetCoordinator,
7347
7519
  fingerprintAgentHarness as fingerprintAgentHarness2,
@@ -8871,10 +9043,10 @@ class TaskBudget {
8871
9043
  this.accountRunId = TaskBudget.accountId(scope, taskId);
8872
9044
  this.policy = structuredClone(account.policy);
8873
9045
  const budgetId = this.accountRunId;
8874
- this.coordinator = createAgentBudgetCoordinator({ store, scope, budgetId, limits: this.policy.limits });
9046
+ this.coordinator = createAgentBudgetCoordinator({ store: bindBudgetLedgerStore(store, scope), scope, budgetId, limits: this.policy.limits });
8875
9047
  this.coordinatorId = this.coordinator.id;
8876
9048
  this.budgetRunId = `budget_${fingerprintAgentHarness2({ budgetId, scope }).slice("sha256:".length)}`;
8877
- this.budgetScope = { tenantId: scope.tenantId, ...scope.userId ? { userId: scope.userId } : {}, namespace: "__zhivex_budget__" };
9049
+ this.budgetScope = budgetLedgerScope(scope);
8878
9050
  }
8879
9051
  static accountId(scope, taskId) {
8880
9052
  return `task_budget_${fingerprintAgentHarness2({ scope, taskId }).slice("sha256:".length)}`;
@@ -12378,6 +12550,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
12378
12550
  var LOCAL_TOOLS = LOCAL_TOOL_NAMES;
12379
12551
  var MAX_USER_STEERING = 10;
12380
12552
  var CONTEXT_PRIORITY = "Latest user > chronological user steering > historical objective. Assistant never overrides user facts. Questions retain task.";
12553
+ var EARLIER = "[before later user messages] ";
12381
12554
  var summarizeHarnessMessages = (messages, maxCharacters = 4000) => {
12382
12555
  const redaction = createRedactionPolicy6({ includeEmails: true });
12383
12556
  const clean = (text3, limit = 512) => redaction.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);
@@ -12403,6 +12576,12 @@ var summarizeHarnessMessages = (messages, maxCharacters = 4000) => {
12403
12576
  omitted = true;
12404
12577
  }
12405
12578
  };
12579
+ const markRecentEarlier = () => {
12580
+ const marked = recent.map((value) => value.startsWith(EARLIER) ? value : `${EARLIER}${value}`);
12581
+ recent.length = 0;
12582
+ for (const value of marked)
12583
+ add(recent, value, 4);
12584
+ };
12406
12585
  const rememberObservation = (tool3, detail) => {
12407
12586
  if (typeof tool3 !== "string" || !LOCAL_TOOLS.has(tool3) || typeof detail !== "string")
12408
12587
  return;
@@ -12520,6 +12699,7 @@ var summarizeHarnessMessages = (messages, maxCharacters = 4000) => {
12520
12699
  objective = text3;
12521
12700
  if (historicalObjective)
12522
12701
  add(steering, text3, MAX_USER_STEERING);
12702
+ markRecentEarlier();
12523
12703
  } else
12524
12704
  add(recent, `${message.role}: ${text3}`, 4);
12525
12705
  } else if (part.type === "tool-call") {
@@ -13762,37 +13942,7 @@ import {
13762
13942
  createSqliteAgentRunStore
13763
13943
  } from "@zhivex-ai/agents/ops";
13764
13944
  import { createAgentRunLedger } from "@zhivex-ai/agents/control-plane";
13765
- var budgetScopeFor = (config) => ({
13766
- tenantId: config.scope.tenantId,
13767
- ...config.scope.userId ? { userId: config.scope.userId } : {},
13768
- namespace: "__zhivex_budget__"
13769
- });
13770
- var withBudgetLedgerStore = (primary, budget, config) => {
13771
- const scope = budgetScopeFor(config);
13772
- const budgetScope = (value) => value?.namespace === scope.namespace && value.tenantId === scope.tenantId && (value.userId ?? undefined) === (scope.userId ?? undefined);
13773
- const assertId = (id) => {
13774
- if (!/^budget_[a-f0-9]{64}$/.test(id))
13775
- throw new HarnessStateConflictError("Invalid budget ledger identity.");
13776
- };
13777
- return {
13778
- ...primary,
13779
- load: async (id, requestedScope) => {
13780
- if (!budgetScope(requestedScope))
13781
- return primary.load(id, requestedScope);
13782
- assertId(id);
13783
- return budget.load(id, requestedScope);
13784
- },
13785
- save: async (state, options) => {
13786
- if (!budgetScope(state.scope))
13787
- return primary.save(state, options);
13788
- assertId(state.runId);
13789
- if (state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true) {
13790
- throw new HarnessStateConflictError("Only SDK budget ledger states may use the reserved namespace.");
13791
- }
13792
- return budget.save(state, options);
13793
- }
13794
- };
13795
- };
13945
+ var withBudgetLedgerStore = (primary, budget, config) => bindBudgetLedgerStore(primary, config.scope, budget);
13796
13946
  var HARNESS_SQLITE_FILE = "operations.sqlite";
13797
13947
  var HARNESS_OPERATIONS_SCHEMA_VERSION = 1;
13798
13948
  var runSummary = (state) => {
@@ -13963,13 +14113,12 @@ var migrateLegacyFileRuns = async (directory, target, config) => {
13963
14113
  return result;
13964
14114
  };
13965
14115
  var openHarnessPersistence = async (config, options = {}) => {
13966
- if (config.scope.namespace === "__zhivex_budget__")
13967
- throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
14116
+ const budgetScope = budgetLedgerScope(config.scope);
13968
14117
  await validateStateDirectory(config.workspace, config.stateDirectory);
13969
14118
  await ensurePrivateStateDirectory(config.stateDirectory);
13970
14119
  await protectStateFromGit(config.workspace, config.stateDirectory);
13971
14120
  if (config.storeBackend === "file") {
13972
- const store = withBudgetLedgerStore(createFileAgentRunStore({ directory: config.stateDirectory, scope: config.scope }), createFileAgentRunStore({ directory: config.stateDirectory, scope: budgetScopeFor(config) }), config);
14121
+ const store = withBudgetLedgerStore(createFileAgentRunStore({ directory: config.stateDirectory, scope: config.scope }), createFileAgentRunStore({ directory: config.stateDirectory, scope: budgetScope }), config);
13973
14122
  const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
13974
14123
  return {
13975
14124
  backend: "file",
@@ -14014,7 +14163,7 @@ var openHarnessPersistence = async (config, options = {}) => {
14014
14163
  database.exec("PRAGMA foreign_keys = ON");
14015
14164
  await chmod3(databasePath, 384);
14016
14165
  const databaseLike = sqliteAdapter(database);
14017
- const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
14166
+ const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScope }), config);
14018
14167
  database.exec(`CREATE TRIGGER IF NOT EXISTS zhivex_task_budget_retain_run
14019
14168
  BEFORE DELETE ON zhivex_agent_runs
14020
14169
  WHEN json_type(OLD.state_json, '$.metadata.zhivexTaskBudgetAccountV1') IS NOT NULL
@@ -14105,7 +14254,7 @@ var projectMemoryBinding = (workspace, scope) => {
14105
14254
  segment(scope.userId);
14106
14255
  if (scope.namespace !== undefined)
14107
14256
  segment(scope.namespace);
14108
- if (scope.namespace === "__zhivex_budget__")
14257
+ if (isReservedBudgetNamespace(scope.namespace))
14109
14258
  throw new HarnessConfigError("Reserved memory namespace.");
14110
14259
  const key = (kind, value) => createHash24("sha256").update(kind).update("\x00").update(value).digest("hex");
14111
14260
  const workspaceKey = key("workspace", workspace);
@@ -14663,7 +14812,8 @@ var resolvers = {
14663
14812
  const parsed = moveFileInputSchema.parse(input);
14664
14813
  return [parsed.source, parsed.destination];
14665
14814
  },
14666
- quarantine_file: (input) => [quarantineFileInputSchema.parse(input).path]
14815
+ quarantine_file: (input) => [quarantineFileInputSchema.parse(input).path],
14816
+ restore_file: (input) => [restoreFileInputSchema.parse(input).destination]
14667
14817
  };
14668
14818
  var validateBuiltinToolPolicy = (policy, tools) => {
14669
14819
  for (const rule of policy.rules)
@@ -14797,7 +14947,7 @@ import { z as z32 } from "zod";
14797
14947
  // package.json
14798
14948
  var package_default = {
14799
14949
  name: "@zhivex-ai/harness",
14800
- version: "1.4.1",
14950
+ version: "1.6.2",
14801
14951
  description: "Governed, provider-portable runtime for coding agents with durable approvals and isolated repository execution.",
14802
14952
  type: "module",
14803
14953
  license: "MIT",
@@ -14897,6 +15047,7 @@ var package_default = {
14897
15047
  "LICENSE",
14898
15048
  "docs/CHANGE_ENVELOPES.md",
14899
15049
  "docs/CLI.md",
15050
+ "docs/AUTO_APPROVE.md",
14900
15051
  "docs/WEB_LIMITS.md",
14901
15052
  "docs/CODE_RELEASE.md",
14902
15053
  "docs/CODE_WEB_TASKS.md",
@@ -15013,26 +15164,26 @@ var package_default = {
15013
15164
  "smoke:mcp:installed": "bun run build && bun scripts/mcp-installed-smoke.ts"
15014
15165
  },
15015
15166
  dependencies: {
15016
- "@zhivex-ai/agents": "1.11.0",
15017
- "@zhivex-ai/anthropic": "0.14.0",
15018
- "@zhivex-ai/azure-openai": "0.7.8",
15019
- "@zhivex-ai/bedrock": "2.0.7",
15020
- "@zhivex-ai/core": "1.34.0",
15021
- "@zhivex-ai/gemini": "0.13.0",
15022
- "@zhivex-ai/meta": "0.2.9",
15023
- "@zhivex-ai/openai": "0.16.0",
15024
- "@zhivex-ai/qwen": "0.16.4",
15025
- "@zhivex-ai/vertex": "1.2.7",
15026
- "google-auth-library": "11.1.0",
15027
- zod: "4.6.5"
15167
+ "@zhivex-ai/agents": "1.12.0",
15168
+ "@zhivex-ai/anthropic": "0.15.0",
15169
+ "@zhivex-ai/azure-openai": "0.8.0",
15170
+ "@zhivex-ai/bedrock": "2.1.0",
15171
+ "@zhivex-ai/core": "1.36.0",
15172
+ "@zhivex-ai/gemini": "0.14.0",
15173
+ "@zhivex-ai/meta": "0.3.0",
15174
+ "@zhivex-ai/openai": "0.17.0",
15175
+ "@zhivex-ai/qwen": "0.18.0",
15176
+ "@zhivex-ai/vertex": "1.3.0",
15177
+ "google-auth-library": "11.3.1",
15178
+ zod: "4.6.7"
15028
15179
  },
15029
15180
  overrides: {
15030
- "@zhivex-ai/core": "1.34.0"
15181
+ "@zhivex-ai/core": "1.36.0"
15031
15182
  },
15032
15183
  devDependencies: {
15033
15184
  "@agentclientprotocol/sdk": "1.8.0",
15034
15185
  "@modelcontextprotocol/server": "2.3.1",
15035
- "@types/bun": "1.4.2",
15186
+ "@types/bun": "1.4.3",
15036
15187
  "@types/node": "^26.6.5",
15037
15188
  typescript: "7.0.2",
15038
15189
  "typescript-compiler-api": "npm:typescript@6.0.3"
@@ -16322,7 +16473,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
16322
16473
  }),
16323
16474
  restore_file: tool5({
16324
16475
  name: "restore_file",
16325
- description: "Restore a quarantined file to its original path or an explicit safe destination without overwriting content unexpectedly.",
16476
+ 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.",
16326
16477
  schema: restoreFileInputSchema,
16327
16478
  ...mutationApproval,
16328
16479
  execute: async (input, context) => serializeJsonValue9(editContractDocument("restore-result", await (harnessExecutionSession(context)?.workspace ?? workspace).restoreQuarantined(input)))
@@ -3,15 +3,15 @@ import {
3
3
  combineMcpHostSessions,
4
4
  createHarnessMcpTools,
5
5
  issueMcpHostSession
6
- } from "./index-dtb2c6ds.js";
6
+ } from "./index-2r4f0p4n.js";
7
7
  import {
8
8
  collectSnapshotInventory,
9
9
  readSnapshotFile,
10
10
  runPortableProcess
11
- } from "./index-d9zkmhz3.js";
11
+ } from "./index-w0m1nckc.js";
12
12
  import {
13
13
  readRegularFileNoFollow
14
- } from "./index-xdbkcccf.js";
14
+ } from "./index-6dxrbnxm.js";
15
15
 
16
16
  // src/execution/mcp-workspace-snapshot.ts
17
17
  import { createHash } from "node:crypto";
@@ -1254,6 +1254,38 @@ var resultSchema = z3.object({
1254
1254
  isError: z3.boolean().optional(),
1255
1255
  _meta: record.optional()
1256
1256
  }).strict().refine((value) => value.content !== undefined || value.structuredContent !== undefined);
1257
+ var ENCODED_MARK = "[ENCODED]";
1258
+ var ENTROPY_MARK = "[HIGH_ENTROPY]";
1259
+ function shannonEntropy(value) {
1260
+ const counts = new Map;
1261
+ for (const char of value)
1262
+ counts.set(char, (counts.get(char) ?? 0) + 1);
1263
+ let entropy = 0;
1264
+ for (const count of counts.values()) {
1265
+ const p = count / value.length;
1266
+ entropy -= p * Math.log2(p);
1267
+ }
1268
+ return entropy;
1269
+ }
1270
+ function secretEncodings(secret) {
1271
+ if (Buffer.byteLength(secret) < 8)
1272
+ return [];
1273
+ const bytes = Buffer.from(secret, "utf8");
1274
+ const base64 = bytes.toString("base64");
1275
+ const base64Url = bytes.toString("base64url");
1276
+ const hex = bytes.toString("hex");
1277
+ return [...new Set([base64, base64.replace(/=+$/u, ""), base64Url, hex, hex.toUpperCase()])].filter((value) => value.length >= 8);
1278
+ }
1279
+ function flagEncodedSecrets(value, secrets) {
1280
+ let text = value;
1281
+ for (const encoding of secrets.flatMap(secretEncodings).sort((left, right) => right.length - left.length)) {
1282
+ text = text.split(encoding).join(ENCODED_MARK);
1283
+ }
1284
+ const flag = (token) => shannonEntropy(token.replace(/=+$/u, "")) >= 3.5 ? ENTROPY_MARK : token;
1285
+ text = text.replace(/[A-Za-z0-9+/_-]{24,}={0,2}/g, flag);
1286
+ text = text.replace(/\b[a-fA-F0-9]{32,}\b/g, flag);
1287
+ return text;
1288
+ }
1257
1289
  function compileSchema(schema) {
1258
1290
  const pending = [{ value: schema, depth: 0 }];
1259
1291
  let nodes = 0;
@@ -1286,21 +1318,25 @@ async function createIsolatedMcpTools(options) {
1286
1318
  throw new McpStdioClientError("protocol", message, effectful);
1287
1319
  };
1288
1320
  const secrets = [...new Set(options.sensitiveValues ?? [])].filter(Boolean).sort((a, b) => b.length - a.length);
1321
+ const flagResults = options.flagEncodedSecrets === true;
1289
1322
  const redaction = createRedactionPolicy({ includeEmails: true });
1290
- const text = (value) => redaction.redactText(secrets.reduce((current, secret) => current.split(secret).join("[REDACTED]"), value));
1291
- const scrub = (value, depth = 0, budget = { nodes: 0 }) => {
1323
+ const text = (value, flag) => {
1324
+ const redacted = redaction.redactText(secrets.reduce((current, secret) => current.split(secret).join("[REDACTED]"), value));
1325
+ return flag ? flagEncodedSecrets(redacted, secrets) : redacted;
1326
+ };
1327
+ const scrub = (value, flag, depth = 0, budget = { nodes: 0 }) => {
1292
1328
  if (depth > 32 || ++budget.nodes > 20000)
1293
1329
  throw new Error("MCP payload complexity limit exceeded.");
1294
1330
  if (typeof value === "string")
1295
- return text(value);
1331
+ return text(value, flag);
1296
1332
  if (Array.isArray(value))
1297
- return value.map((item) => scrub(item, depth + 1, budget));
1333
+ return value.map((item) => scrub(item, flag, depth + 1, budget));
1298
1334
  if (value && typeof value === "object")
1299
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [text(key), scrub(item, depth + 1, budget)]));
1335
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [text(key, flag), scrub(item, flag, depth + 1, budget)]));
1300
1336
  return value;
1301
1337
  };
1302
- const bounded = (value) => {
1303
- const clean = scrub(value);
1338
+ const bounded = (value, flag) => {
1339
+ const clean = scrub(value, flag);
1304
1340
  if (Buffer.byteLength(JSON.stringify(value)) > proposal.limits.maxOutputBytes)
1305
1341
  throw new Error("MCP output limit exceeded.");
1306
1342
  return clean;
@@ -1319,7 +1355,7 @@ async function createIsolatedMcpTools(options) {
1319
1355
  const output = outputValidators.get(input.name);
1320
1356
  if (output && !parsed.isError && !output.safeParse(parsed.structuredContent).success)
1321
1357
  throw new Error("Output schema");
1322
- return bounded(parsed);
1358
+ return bounded(parsed, flagResults);
1323
1359
  } catch {
1324
1360
  return invalidResponse("Isolated MCP result validation failed.", true);
1325
1361
  }
@@ -1330,7 +1366,7 @@ async function createIsolatedMcpTools(options) {
1330
1366
  async listTools(input, callOptions) {
1331
1367
  try {
1332
1368
  const response = await options.client.listTools(input, callOptions);
1333
- const clean = bounded(response);
1369
+ const clean = bounded(response, false);
1334
1370
  if (JSON.stringify(clean) !== JSON.stringify(response))
1335
1371
  throw new Error("Sensitive discovery");
1336
1372
  const parsed = listing.parse(response);
@@ -1722,7 +1758,13 @@ async function launchMcpToolsOwned(options, groupRecoveryComplete) {
1722
1758
  return value;
1723
1759
  } });
1724
1760
  try {
1725
- const tools = await createIsolatedMcpTools({ client, proposal, sensitiveValues, journal });
1761
+ const tools = await createIsolatedMcpTools({
1762
+ client,
1763
+ proposal,
1764
+ sensitiveValues,
1765
+ journal,
1766
+ flagEncodedSecrets: Object.keys(proposal.secretReferences).length > 0
1767
+ });
1726
1768
  const hostSession = issueMcpHostSession({ tools, ...journal, proposal, close: () => client.close() });
1727
1769
  return Object.freeze({ tools, hostSession, close: () => closeMcpHostSession(hostSession) });
1728
1770
  } catch {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  HARNESS_VERSION
3
- } from "./index-4fx5f3k5.js";
3
+ } from "./index-rz7wmp71.js";
4
4
  import {
5
5
  readRegularFileNoFollow
6
- } from "./index-xdbkcccf.js";
6
+ } from "./index-6dxrbnxm.js";
7
7
  import {
8
8
  HarnessConfigError,
9
9
  HarnessError,
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  withWorkspaceMutation
3
- } from "./index-d9zkmhz3.js";
3
+ } from "./index-w0m1nckc.js";
4
4
  import {
5
5
  TERMINAL_SESSION_RUN_STATUSES
6
- } from "./index-zxg4ve4j.js";
6
+ } from "./index-fqqz1yt8.js";
7
7
  import {
8
8
  SqliteDatabase,
9
9
  createEditProposal,
@@ -11,7 +11,7 @@ import {
11
11
  readRegularFileNoFollow,
12
12
  statRegularFileNoFollow,
13
13
  workspaceFilePathSchema
14
- } from "./index-xdbkcccf.js";
14
+ } from "./index-6dxrbnxm.js";
15
15
 
16
16
  // src/persistence/workspace-checkpoints.ts
17
17
  import { createHash, randomUUID } from "node:crypto";