@zhivex-ai/harness 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +3 -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/audit/index.js +6 -6
  9. package/dist/{audit-pack-e4vaq7np.js → audit-pack-kvw7ng34.js} +3 -3
  10. package/dist/{cli-entry-rgs6bxmy.js → cli-entry-75j3r49b.js} +2 -2
  11. package/dist/{cli-entry-7f0cen32.js → cli-entry-928p8h41.js} +135 -58
  12. package/dist/{cli-entry-a890x3kq.js → cli-entry-h2yc9x37.js} +7 -5
  13. package/dist/{cli-entry-3s63zn4w.js → cli-entry-q8bgkn9m.js} +1 -1
  14. package/dist/{cli-1cf4dcnj.js → cli-vyrytn3m.js} +14 -8
  15. package/dist/cli.js +2 -2
  16. package/dist/compat/code-support.js +8 -8
  17. package/dist/engine/acp.js +11 -11
  18. package/dist/engine/client.js +10 -10
  19. package/dist/engine/desktop-state-v1.js +6 -6
  20. package/dist/engine/index.js +15 -15
  21. package/dist/engine/mcp-stdio-v1.js +5 -5
  22. package/dist/engine/service.js +11 -11
  23. package/dist/hostile-repository-demo.js +138 -60
  24. package/dist/{index-dtb2c6ds.js → index-5zbq9ztj.js} +2 -2
  25. package/dist/{index-6599mpmy.js → index-63gbp0cz.js} +3 -3
  26. package/dist/{index-xdbkcccf.js → index-6dxrbnxm.js} +2 -2
  27. package/dist/{index-0zs2dz26.js → index-6fp7kdpx.js} +10 -9
  28. package/dist/{index-mv8sb1ns.js → index-96ezrvg2.js} +55 -13
  29. package/dist/{index-wm9xeq1n.js → index-bt2p8x7r.js} +1 -1
  30. package/dist/{index-sk3cap35.js → index-eps3c3g6.js} +1 -1
  31. package/dist/{index-zxg4ve4j.js → index-fqqz1yt8.js} +77 -4
  32. package/dist/{index-d9zkmhz3.js → index-ggstwr0v.js} +53 -18
  33. package/dist/{index-j6zxnc8m.js → index-jpdvn09j.js} +5 -5
  34. package/dist/{index-4fx5f3k5.js → index-pq3kz1r8.js} +1 -1
  35. package/dist/{index-02pqtbbg.js → index-vc7yxz50.js} +10 -10
  36. package/dist/{index-crta5wzv.js → index-vvzc5ezz.js} +4 -4
  37. package/dist/{index-f6mqa5gk.js → index-wwthv10a.js} +11 -39
  38. package/dist/{index-74kkpyqk.js → index-xfedewpt.js} +12 -6
  39. package/dist/{index-sstgcsr0.js → index-yx52bt4a.js} +1 -1
  40. package/dist/index.js +14 -14
  41. package/dist/integrations/mcp-stdio-tools.d.ts +4 -1
  42. package/dist/runtime/budget-ledger-scope.d.ts +14 -0
  43. package/dist/service-cli.js +11 -11
  44. package/dist/{service-client-cli-eqrdc9zq.js → service-client-cli-v6qptg9x.js} +4 -4
  45. package/dist/tools/workspace.d.ts +2 -2
  46. package/dist/verify-historical-migrations.js +143 -62
  47. package/dist/workspace/edit-contracts.d.ts +2 -2
  48. package/dist/workspace/path-policy.d.ts +2 -1
  49. package/dist/workspace/workspace.d.ts +13 -0
  50. package/dist/zhx.js +2 -2
  51. package/docs/CODE_RELEASE.md +28 -6
  52. package/docs/CODE_WEB_TASKS.md +1 -1
  53. package/docs/ENGINE_API.md +1 -1
  54. package/docs/EXTENSIBILITY.md +6 -2
  55. package/docs/FIRST_USE.md +6 -6
  56. package/docs/PROJECT_MEMORY.md +1 -1
  57. package/docs/PUBLIC_SECURITY.md +14 -1
  58. package/docs/REPOSITORY_EDITING.md +1 -1
  59. package/docs/TOOL_POLICY.md +4 -1
  60. package/docs/adr/0002-isolated-mcp-stdio.md +1 -1
  61. package/evaluations/representative-assembly-matrix.json +10 -1
  62. package/examples/README.md +3 -3
  63. package/package.json +1 -1
@@ -1,12 +1,15 @@
1
1
  import { type McpClient, type ToolSet } from '@zhivex-ai/core';
2
2
  import type { McpStdioLaunchProposal } from './mcp-stdio-admission.js';
3
3
  import { createJournaledMcpClient } from '../persistence/mcp-execution-journal.js';
4
- /** Host tool bridge. Existing agent execution supplies durable interrupt approvals. */
4
+ /** Host tool bridge. Tool results are untrusted data. Existing agent execution supplies durable interrupt approvals.
5
+ * flagEncodedSecrets is optional. Credential-bearing servers use it to mark encodings and high-entropy tokens; it does not rewrite discovery schemas. */
5
6
  export declare function createIsolatedMcpTools(options: {
6
7
  client: McpClient & {
7
8
  close(): Promise<void>;
8
9
  };
9
10
  proposal: Readonly<McpStdioLaunchProposal>;
10
11
  sensitiveValues?: readonly string[];
12
+ /** When set, tool results flag known encodings of sensitiveValues and other high-entropy encoded strings. */
13
+ flagEncodedSecrets?: boolean;
11
14
  journal?: Pick<Parameters<typeof createJournaledMcpClient>[0], 'store' | 'runId' | 'scope'>;
12
15
  }): Promise<ToolSet>;
@@ -0,0 +1,14 @@
1
+ import type { AgentRunStore, AgentStoreScope } from "@zhivex-ai/core";
2
+ /** SDK coordinators address ledgers with this namespace. The durable owner appends the source namespace. */
3
+ export declare const BUDGET_LEDGER_NAMESPACE = "__zhivex_budget__";
4
+ export declare const BUDGET_OWNER_NAMESPACE_KEY = "budgetOwnerNamespace";
5
+ export declare const isReservedBudgetNamespace: (namespace: string | undefined) => boolean;
6
+ /** Bind the caller's full namespace into the ledger owner. Collapsing every namespace to the SDK marker lets one tenant read another's ledger by run id. */
7
+ export declare const budgetLedgerNamespace: (namespace: string) => string;
8
+ /**
9
+ * A named source becomes `__zhivex_budget__:<namespace>`.
10
+ * An omitted namespace uses the trailing-colon owner, which `budgetLedgerNamespace` never produces, so it cannot join a named ledger.
11
+ */
12
+ export declare const budgetLedgerScope: (scope: Pick<AgentStoreScope, "tenantId" | "userId" | "namespace">) => AgentStoreScope;
13
+ /** Translate the SDK's collapsed namespace onto the bound owner before the file or SQLite store. */
14
+ export declare const bindBudgetLedgerStore: (primary: AgentRunStore, scope: AgentStoreScope, budget?: AgentRunStore) => AgentRunStore;
@@ -2,22 +2,22 @@
2
2
  import {
3
3
  recoverHarnessLocalService,
4
4
  startHarnessLocalService
5
- } from "./index-crta5wzv.js";
6
- import"./index-02pqtbbg.js";
7
- import"./index-6599mpmy.js";
5
+ } from "./index-vvzc5ezz.js";
6
+ import"./index-vc7yxz50.js";
7
+ import"./index-63gbp0cz.js";
8
8
  import {
9
9
  createHarness
10
- } from "./index-0zs2dz26.js";
11
- import"./index-f6mqa5gk.js";
12
- import"./index-dtb2c6ds.js";
13
- import"./index-d9zkmhz3.js";
10
+ } from "./index-6fp7kdpx.js";
11
+ import"./index-wwthv10a.js";
12
+ import"./index-5zbq9ztj.js";
13
+ import"./index-ggstwr0v.js";
14
14
  import"./index-1265j7t2.js";
15
15
  import"./index-c39197wq.js";
16
16
  import"./index-mzydbzp8.js";
17
- import"./index-wm9xeq1n.js";
18
- import"./index-zxg4ve4j.js";
19
- import"./index-4fx5f3k5.js";
20
- import"./index-xdbkcccf.js";
17
+ import"./index-bt2p8x7r.js";
18
+ import"./index-fqqz1yt8.js";
19
+ import"./index-pq3kz1r8.js";
20
+ import"./index-6dxrbnxm.js";
21
21
  import"./index-4gt045j0.js";
22
22
  import"./index-bpc596v3.js";
23
23
  import"./index-fm84men1.js";
@@ -9,17 +9,17 @@ import {
9
9
  navigateConsole,
10
10
  printPolicyInspection,
11
11
  sanitizeTerminalText
12
- } from "./cli-entry-a890x3kq.js";
12
+ } from "./cli-entry-h2yc9x37.js";
13
13
  import {
14
14
  CLI_EVENT_SCHEMA_VERSION,
15
15
  CLI_JSON_SCHEMA_VERSION,
16
16
  changeEnvelopeSchema,
17
17
  harnessPolicyDecisionEventSchema,
18
18
  serializeStreamResult
19
- } from "./cli-entry-7f0cen32.js";
19
+ } from "./cli-entry-928p8h41.js";
20
20
  import {
21
21
  HARNESS_VERSION
22
- } from "./cli-entry-3s63zn4w.js";
22
+ } from "./cli-entry-q8bgkn9m.js";
23
23
  import {
24
24
  HARNESS_ERROR_CODES,
25
25
  HARNESS_ERROR_SCHEMA_VERSION,
@@ -195,7 +195,7 @@ var harnessTaskProjectionSchema = z2.strictObject({
195
195
  // src/approvals/approval-diff.ts
196
196
  import { z as z3 } from "zod";
197
197
  var sha = z3.string().regex(/^sha256:[a-f0-9]{64}$/);
198
- var file = z3.object({ path: z3.string().max(1024), expectedDigest: sha.nullable(), before: z3.string().nullable(), after: z3.string().nullable(), afterDigest: sha.nullable(), beforeMode: z3.number().int().optional(), afterMode: z3.number().int().optional(), operation: z3.enum(["create", "update", "delete"]).optional() }).strict();
198
+ var file = z3.object({ path: z3.string().max(1024), expectedDigest: sha.nullable(), before: z3.string().nullable(), after: z3.string().nullable(), afterDigest: sha.nullable(), beforeMode: z3.number().int().optional(), afterMode: z3.number().int().optional(), operation: z3.enum(["create", "update", "delete", "restore"]).optional() }).strict();
199
199
  var archive = z3.object({ approvalId: z3.string().max(256), digest: z3.string().regex(/^[a-f0-9]{64}$/), proposalId: sha, files: z3.array(file).min(1).max(50) }).strict();
200
200
  var archives = z3.array(archive).max(512);
201
201
  var maxBytes = 2 * 1024 * 1024;
@@ -89,8 +89,8 @@ export declare const createWorkspaceTools: (workspace: Workspace, allowedChecks:
89
89
  }, z.core.$strict>, import("@zhivex-ai/core").JsonValue, unknown>;
90
90
  restore_file: import("@zhivex-ai/core").ToolDefinition<z.ZodObject<{
91
91
  quarantineId: z.ZodString;
92
- destination: z.ZodOptional<z.ZodString>;
93
- expectedDigest: z.ZodOptional<z.ZodString>;
92
+ destination: z.ZodString;
93
+ expectedDigest: z.ZodString;
94
94
  }, z.core.$strict>, import("@zhivex-ai/core").JsonValue, unknown>;
95
95
  run_check: import("@zhivex-ai/core").ToolDefinition<z.ZodObject<{
96
96
  check: z.ZodString;
@@ -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({
@@ -5908,10 +5908,10 @@ var runPortableProcess = async (command, options = {}) => {
5908
5908
  // src/workspace/path-policy.ts
5909
5909
  var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
5910
5910
  var isSensitiveName = (name) => {
5911
- const normalized = name.toLocaleLowerCase();
5911
+ const normalized = name.toLowerCase();
5912
5912
  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
5913
  };
5914
- var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
5914
+ var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
5915
5915
 
5916
5916
  // src/workspace/workspace.ts
5917
5917
  var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
@@ -6191,7 +6191,7 @@ class Workspace {
6191
6191
  throw new HarnessWorkspaceError(`The path escapes the workspace: ${relativePath}`);
6192
6192
  const relative = wirePath(path11.relative(this.root, candidate));
6193
6193
  if (relative && isHardIgnored(relative)) {
6194
- const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
6194
+ const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLowerCase()) || isSensitiveName(segment));
6195
6195
  throw new HarnessWorkspaceError(`The path is protected by the harness policy: ${blocked}`);
6196
6196
  }
6197
6197
  return candidate;
@@ -7174,30 +7174,65 @@ class Workspace {
7174
7174
  const audit = await this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
7175
7175
  return { quarantineId, path: parsed.path, digest: source.digest, audit };
7176
7176
  }
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();
7177
+ async readRestorableQuarantine(quarantineId) {
7183
7178
  const directory = await this.secureQuarantineDirectory(false);
7184
7179
  let manifest;
7185
7180
  try {
7186
- const manifestFile = await this.readInternalRegularFile(path11.join(directory, `${parsed.quarantineId}.json`), "Quarantine manifest", 64 * 1024);
7181
+ const manifestFile = await this.readInternalRegularFile(path11.join(directory, `${quarantineId}.json`), "Quarantine manifest", 64 * 1024);
7187
7182
  manifest = JSON.parse(manifestFile.contents.toString("utf8"));
7188
7183
  } catch {
7189
- throw new HarnessWorkspaceError(`Quarantine entry was not found: ${parsed.quarantineId}.`);
7184
+ throw new HarnessWorkspaceError(`Quarantine entry was not found: ${quarantineId}.`);
7190
7185
  }
7191
- if (manifest.schemaVersion !== 1 || manifest.quarantineId !== parsed.quarantineId || manifest.status !== "quarantined") {
7192
- throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${parsed.quarantineId}.`);
7186
+ if (manifest.schemaVersion !== 1 || manifest.quarantineId !== quarantineId || manifest.status !== "quarantined") {
7187
+ throw new HarnessWorkspaceError(`Quarantine entry is not restorable: ${quarantineId}.`);
7193
7188
  }
7194
- const dataPath = path11.join(directory, `${parsed.quarantineId}.data`);
7189
+ const dataPath = path11.join(directory, `${quarantineId}.data`);
7195
7190
  const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
7196
7191
  const digest3 = digestBytes(dataFile.contents);
7197
- if (digest3 !== manifest.digest || parsed.expectedDigest && parsed.expectedDigest !== digest3) {
7192
+ if (digest3 !== manifest.digest) {
7193
+ throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${quarantineId}.`);
7194
+ }
7195
+ return { manifest, dataPath, dataFile, digest: digest3 };
7196
+ }
7197
+ async previewRestore(input) {
7198
+ const parsed = restoreFileInputSchema.parse(input);
7199
+ const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
7200
+ if (parsed.expectedDigest !== quarantined.digest) {
7198
7201
  throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
7199
7202
  }
7200
- const destinationPath = parsed.destination ?? manifest.originalPath;
7203
+ const destination = await this.safePath(parsed.destination, { allowMissing: true });
7204
+ if (destination.exists)
7205
+ throw new HarnessWorkspaceError(`Restore destination already exists: ${parsed.destination}.`);
7206
+ let after;
7207
+ try {
7208
+ after = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(quarantined.dataFile.contents);
7209
+ } catch {
7210
+ throw new HarnessWorkspaceError("Preview requires valid UTF-8 text.");
7211
+ }
7212
+ return {
7213
+ proposalId: parsed.expectedDigest,
7214
+ files: [{
7215
+ path: parsed.destination,
7216
+ expectedDigest: parsed.expectedDigest,
7217
+ before: null,
7218
+ after,
7219
+ afterDigest: quarantined.digest,
7220
+ operation: "restore"
7221
+ }]
7222
+ };
7223
+ }
7224
+ async restoreQuarantined(input) {
7225
+ return withWorkspaceMutation(this.root, () => this.restoreQuarantinedLocked(input));
7226
+ }
7227
+ async restoreQuarantinedLocked(input) {
7228
+ const parsed = restoreFileInputSchema.parse(input);
7229
+ this.checkAuditCapacity();
7230
+ const quarantined = await this.readRestorableQuarantine(parsed.quarantineId);
7231
+ if (parsed.expectedDigest !== quarantined.digest) {
7232
+ throw new HarnessWorkspaceError(`Quarantine digest mismatch: ${parsed.quarantineId}.`);
7233
+ }
7234
+ const { manifest, dataPath, dataFile, digest: digest3 } = quarantined;
7235
+ const destinationPath = parsed.destination;
7201
7236
  const destination = await this.safePath(destinationPath, { allowMissing: true });
7202
7237
  if (destination.exists)
7203
7238
  throw new HarnessWorkspaceError(`Restore destination already exists: ${destinationPath}.`);
@@ -8420,7 +8455,7 @@ var importPatch = async (runId, expectedPatchId, host, base, current2, maxFileWr
8420
8455
  const rollbackErrors = [];
8421
8456
  for (const item of [...quarantined].reverse()) {
8422
8457
  try {
8423
- await host.restoreQuarantined({ quarantineId: item.quarantineId, expectedDigest: item.digest });
8458
+ await host.restoreQuarantined({ quarantineId: item.quarantineId, destination: item.path, expectedDigest: item.digest });
8424
8459
  } catch (rollbackError) {
8425
8460
  rollbackErrors.push(rollbackError);
8426
8461
  }
@@ -9718,6 +9753,79 @@ import { createAgentRunLedger } from "@zhivex-ai/agents/control-plane";
9718
9753
  import { AsyncLocalStorage as AsyncLocalStorage5 } from "node:async_hooks";
9719
9754
  import { randomUUID as randomUUID6 } from "node:crypto";
9720
9755
  import { z as z18 } from "zod";
9756
+
9757
+ // src/runtime/budget-ledger-scope.ts
9758
+ var BUDGET_LEDGER_NAMESPACE = "__zhivex_budget__";
9759
+ var BUDGET_OWNER_NAMESPACE_KEY = "budgetOwnerNamespace";
9760
+ var isReservedBudgetNamespace = (namespace) => namespace === BUDGET_LEDGER_NAMESPACE || (namespace?.startsWith(`${BUDGET_LEDGER_NAMESPACE}:`) ?? false);
9761
+ var budgetLedgerNamespace = (namespace) => {
9762
+ if (!namespace || isReservedBudgetNamespace(namespace)) {
9763
+ throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
9764
+ }
9765
+ return `${BUDGET_LEDGER_NAMESPACE}:${namespace}`;
9766
+ };
9767
+ var budgetLedgerScope = (scope) => {
9768
+ if (scope.namespace && isReservedBudgetNamespace(scope.namespace)) {
9769
+ throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
9770
+ }
9771
+ return {
9772
+ tenantId: scope.tenantId,
9773
+ ...scope.userId ? { userId: scope.userId } : {},
9774
+ namespace: scope.namespace ? budgetLedgerNamespace(scope.namespace) : `${BUDGET_LEDGER_NAMESPACE}:`
9775
+ };
9776
+ };
9777
+ var sameIdentity = (left, right) => left?.tenantId === right.tenantId && (left.userId ?? undefined) === (right.userId ?? undefined);
9778
+ var bindBudgetLedgerStore = (primary, scope, budget = primary) => {
9779
+ const bound = budgetLedgerScope(scope);
9780
+ const ownerNamespace = scope.namespace ?? "";
9781
+ const isSdkBudget = (value) => sameIdentity(value, bound) && value?.namespace === BUDGET_LEDGER_NAMESPACE;
9782
+ const isBoundBudget = (value) => sameIdentity(value, bound) && value?.namespace === bound.namespace;
9783
+ const assertId = (id) => {
9784
+ if (!/^budget_[a-f0-9]{64}$/.test(id))
9785
+ throw new HarnessStateConflictError("Invalid budget ledger identity.");
9786
+ };
9787
+ const bindState = (state) => {
9788
+ const metadata = { ...state.metadata ?? {} };
9789
+ const owner = metadata[BUDGET_OWNER_NAMESPACE_KEY];
9790
+ if (owner !== undefined && owner !== ownerNamespace) {
9791
+ throw new HarnessStateConflictError("Budget ledger is bound to another namespace.");
9792
+ }
9793
+ return {
9794
+ ...state,
9795
+ scope: bound,
9796
+ metadata: { ...metadata, [BUDGET_OWNER_NAMESPACE_KEY]: ownerNamespace }
9797
+ };
9798
+ };
9799
+ const assertStored = (state) => {
9800
+ if (!state)
9801
+ return state;
9802
+ const owner = state.metadata?.[BUDGET_OWNER_NAMESPACE_KEY];
9803
+ if (state.scope?.namespace !== bound.namespace || owner !== ownerNamespace) {
9804
+ throw new HarnessStateConflictError("Budget ledger is not bound to this namespace.");
9805
+ }
9806
+ return state;
9807
+ };
9808
+ return {
9809
+ ...primary,
9810
+ load: async (id, requestedScope) => {
9811
+ if (!isSdkBudget(requestedScope) && !isBoundBudget(requestedScope))
9812
+ return primary.load(id, requestedScope);
9813
+ assertId(id);
9814
+ return assertStored(await budget.load(id, bound));
9815
+ },
9816
+ save: async (state, options) => {
9817
+ if (!isSdkBudget(state.scope) && !isBoundBudget(state.scope))
9818
+ return primary.save(state, options);
9819
+ assertId(state.runId);
9820
+ if (state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true) {
9821
+ throw new HarnessStateConflictError("Only SDK budget ledger states may use the reserved namespace.");
9822
+ }
9823
+ return budget.save(bindState(state), options);
9824
+ }
9825
+ };
9826
+ };
9827
+
9828
+ // src/runtime/task-budget.ts
9721
9829
  import {
9722
9830
  createAgentBudgetCoordinator,
9723
9831
  fingerprintAgentHarness as fingerprintAgentHarness2,
@@ -9836,10 +9944,10 @@ class TaskBudget {
9836
9944
  this.accountRunId = TaskBudget.accountId(scope, taskId);
9837
9945
  this.policy = structuredClone(account.policy);
9838
9946
  const budgetId = this.accountRunId;
9839
- this.coordinator = createAgentBudgetCoordinator({ store, scope, budgetId, limits: this.policy.limits });
9947
+ this.coordinator = createAgentBudgetCoordinator({ store: bindBudgetLedgerStore(store, scope), scope, budgetId, limits: this.policy.limits });
9840
9948
  this.coordinatorId = this.coordinator.id;
9841
9949
  this.budgetRunId = `budget_${fingerprintAgentHarness2({ budgetId, scope }).slice("sha256:".length)}`;
9842
- this.budgetScope = { tenantId: scope.tenantId, ...scope.userId ? { userId: scope.userId } : {}, namespace: "__zhivex_budget__" };
9950
+ this.budgetScope = budgetLedgerScope(scope);
9843
9951
  }
9844
9952
  static accountId(scope, taskId) {
9845
9953
  return `task_budget_${fingerprintAgentHarness2({ scope, taskId }).slice("sha256:".length)}`;
@@ -10354,37 +10462,7 @@ class TaskBudget {
10354
10462
  }
10355
10463
 
10356
10464
  // 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
- };
10465
+ var withBudgetLedgerStore = (primary, budget, config) => bindBudgetLedgerStore(primary, config.scope, budget);
10388
10466
  var HARNESS_SQLITE_FILE = "operations.sqlite";
10389
10467
  var HARNESS_OPERATIONS_SCHEMA_VERSION = 1;
10390
10468
  var runSummary = (state) => {
@@ -10555,13 +10633,12 @@ var migrateLegacyFileRuns = async (directory, target, config) => {
10555
10633
  return result;
10556
10634
  };
10557
10635
  var openHarnessPersistence = async (config, options = {}) => {
10558
- if (config.scope.namespace === "__zhivex_budget__")
10559
- throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
10636
+ const budgetScope = budgetLedgerScope(config.scope);
10560
10637
  await validateStateDirectory(config.workspace, config.stateDirectory);
10561
10638
  await ensurePrivateStateDirectory(config.stateDirectory);
10562
10639
  await protectStateFromGit(config.workspace, config.stateDirectory);
10563
10640
  if (config.storeBackend === "file") {
10564
- const store = withBudgetLedgerStore(createFileAgentRunStore({ directory: config.stateDirectory, scope: config.scope }), createFileAgentRunStore({ directory: config.stateDirectory, scope: budgetScopeFor(config) }), config);
10641
+ const store = withBudgetLedgerStore(createFileAgentRunStore({ directory: config.stateDirectory, scope: config.scope }), createFileAgentRunStore({ directory: config.stateDirectory, scope: budgetScope }), config);
10565
10642
  const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
10566
10643
  return {
10567
10644
  backend: "file",
@@ -10606,7 +10683,7 @@ var openHarnessPersistence = async (config, options = {}) => {
10606
10683
  database.exec("PRAGMA foreign_keys = ON");
10607
10684
  await chmod3(databasePath, 384);
10608
10685
  const databaseLike = sqliteAdapter(database);
10609
- const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
10686
+ const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScope }), config);
10610
10687
  database.exec(`CREATE TRIGGER IF NOT EXISTS zhivex_task_budget_retain_run
10611
10688
  BEFORE DELETE ON zhivex_agent_runs
10612
10689
  WHEN json_type(OLD.state_json, '$.metadata.zhivexTaskBudgetAccountV1') IS NOT NULL
@@ -10774,7 +10851,7 @@ var projectMemoryBinding = (workspace, scope) => {
10774
10851
  segment(scope.userId);
10775
10852
  if (scope.namespace !== undefined)
10776
10853
  segment(scope.namespace);
10777
- if (scope.namespace === "__zhivex_budget__")
10854
+ if (isReservedBudgetNamespace(scope.namespace))
10778
10855
  throw new HarnessConfigError("Reserved memory namespace.");
10779
10856
  const key = (kind, value) => createHash15("sha256").update(kind).update("\x00").update(value).digest("hex");
10780
10857
  const workspaceKey = key("workspace", workspace);
@@ -11166,7 +11243,8 @@ var coordinatorIds = (runs) => new Set(runs.flatMap(({ state }) => {
11166
11243
  var validateBudgetLedger = (config, run, identities) => {
11167
11244
  const state = validatedRunState(run.state, `Budget ledger ${run.key}`);
11168
11245
  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)) {
11246
+ const ownerNamespace = config.scope.namespace;
11247
+ 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
11248
  throw new HarnessStateConflictError("Budget ledger is not bound to an exported run and durable scope.");
11171
11249
  }
11172
11250
  assertTerminalState(state, `Budget ledger ${run.key}`);
@@ -11300,7 +11378,9 @@ var readPayload = async (config, recordedWorkspace, accessLease) => {
11300
11378
  const metadata = state.metadata;
11301
11379
  if (typeof metadata?.budgetIdentity !== "string" || !identities.has(metadata.budgetIdentity)) {
11302
11380
  const scope = state.scope;
11303
- if (scope?.namespace === "__zhivex_budget__" && scope.tenantId === config.scope.tenantId && (scope.userId ?? undefined) === (config.scope.userId ?? undefined)) {
11381
+ const sameTenant = scope?.tenantId === config.scope.tenantId && (scope?.userId ?? undefined) === (config.scope.userId ?? undefined);
11382
+ const boundNamespace = config.scope.namespace ? budgetLedgerNamespace(config.scope.namespace) : undefined;
11383
+ if (sameTenant && (scope?.namespace === BUDGET_LEDGER_NAMESPACE || scope?.namespace === boundNamespace)) {
11304
11384
  throw new HarnessStateConflictError("An unlinked shared budget ledger cannot be safely assigned to this backup scope; retain the original database.");
11305
11385
  }
11306
11386
  return [];
@@ -15140,7 +15220,8 @@ var resolvers = {
15140
15220
  const parsed = moveFileInputSchema.parse(input);
15141
15221
  return [parsed.source, parsed.destination];
15142
15222
  },
15143
- quarantine_file: (input) => [quarantineFileInputSchema.parse(input).path]
15223
+ quarantine_file: (input) => [quarantineFileInputSchema.parse(input).path],
15224
+ restore_file: (input) => [restoreFileInputSchema.parse(input).destination]
15144
15225
  };
15145
15226
  var validateBuiltinToolPolicy = (policy, tools) => {
15146
15227
  for (const rule of policy.rules)
@@ -15274,7 +15355,7 @@ import { z as z34 } from "zod";
15274
15355
  // package.json
15275
15356
  var package_default = {
15276
15357
  name: "@zhivex-ai/harness",
15277
- version: "1.4.1",
15358
+ version: "1.5.0",
15278
15359
  description: "Governed, provider-portable runtime for coding agents with durable approvals and isolated repository execution.",
15279
15360
  type: "module",
15280
15361
  license: "MIT",
@@ -16799,7 +16880,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
16799
16880
  }),
16800
16881
  restore_file: tool5({
16801
16882
  name: "restore_file",
16802
- description: "Restore a quarantined file to its original path or an explicit safe destination without overwriting content unexpectedly.",
16883
+ 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
16884
  schema: restoreFileInputSchema,
16804
16885
  ...mutationApproval,
16805
16886
  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;
@@ -214,6 +214,19 @@ export declare class Workspace {
214
214
  private readInternalRegularFile;
215
215
  quarantineFile(input: QuarantineFileInput, assertActive?: () => Promise<void>): Promise<QuarantineFileResult>;
216
216
  private quarantineFileLocked;
217
+ private readRestorableQuarantine;
218
+ /** Read-only preview of a pinned restore. Destination and digest are required, and nothing is written. */
219
+ previewRestore(input: unknown): Promise<{
220
+ proposalId: string;
221
+ files: {
222
+ path: string;
223
+ expectedDigest: string;
224
+ before: null;
225
+ after: string;
226
+ afterDigest: string;
227
+ operation: "restore";
228
+ }[];
229
+ }>;
217
230
  restoreQuarantined(input: RestoreFileInput): Promise<RestoreFileResult>;
218
231
  private restoreQuarantinedLocked;
219
232
  runCheck(check: HarnessCheck, expectedScript: string, allowedChecks?: readonly string[]): Promise<CommandResult>;
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-q8bgkn9m.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-vyrytn3m.js");
45
45
  await runCli(argv);
46
46
  }
@@ -1,5 +1,16 @@
1
1
  # Code release procedure
2
2
 
3
+ ## Pending patch: 2026-10-16
4
+
5
+ The source version is `1.5.0`, pending publication to `latest`. This checkout
6
+ prepares Harness `1.5.0` and Code `0.4.0` for npm `latest`, with Code pinned
7
+ exactly to the engine and private Desktop bound to the same Harness version.
8
+ Published `latest` is Harness `1.4.1` and Code `0.3.2`. It adds the `v1.5.0`
9
+ representative mapping with the same Meta, Qwen and OpenAI models as `v1.4.1`
10
+ and does not change provider/API tiers. Annotated `v1.5.0` then `code-v0.4.0`
11
+ on the same reviewed main SHA, `channel=latest`, run through the unchanged
12
+ protected workflows and their environment approvals.
13
+
3
14
  ## Pending patch: 2026-10-14
4
15
 
5
16
  This checkout prepares Code `0.3.1`, pinned exactly to Harness `1.4.1`, for npm
@@ -278,9 +289,17 @@ support remain unchanged; estimated budgets are not guaranteed financial caps.
278
289
  verification as ordered in [the Harness release procedure](https://github.com/Zhivex/zhivex-harness/blob/main/docs/RELEASE.md).
279
290
  From a clean checkout of that main commit, create an annotated
280
291
  `code-v0.2.0` tag and push it. Never move or overwrite release tags.
281
- 4. Dispatch `release-code.yml` **at that tag**, with input `tag` matching it and
282
- `channel=latest`, `mode=oidc`, and `confirm_publication=true`. Dispatching
283
- main while supplying a different tag is rejected.
292
+ 4. Dispatch `release-code.yml` **from that exact tag ref**, with input `tag`
293
+ matching it and `channel=latest`, `mode=oidc`, and `confirm_publication=true`.
294
+ A no-permission job is defense in depth, not the trust boundary, because
295
+ GitHub loads it from the dispatched ref. When it still runs, it requires
296
+ `GITHUB_REF` to be exactly `refs/tags/code-v0.2.0`, resolves that annotated
297
+ tag to the workflow commit, and requires the commit to be reachable from
298
+ `main` before provenance OIDC, provider-free validation, or npm publication
299
+ starts. Dispatch from `main` or any other ref is rejected, including when
300
+ the commit matches the tag. Publication still depends on the `npm`
301
+ environment allowing only selected `v*` and `code-v*` tags, and on the tag
302
+ ruleset in [Public repository security](PUBLIC_SECURITY.md).
284
303
 
285
304
  ```sh
286
305
  gh workflow run release-code.yml --ref code-v0.2.0 \
@@ -350,10 +369,13 @@ In npm package settings, configure GitHub Actions with organization `Zhivex`,
350
369
  repository `zhivex-harness`, workflow filename `release-code.yml`, and environment
351
370
  `npm`. Enable the allowed action `npm publish`; the OIDC job uses direct
352
371
  publishing after GitHub approval, not `npm stage publish`. Preserve the GitHub
353
- environment's required human approval. Then prepare a
372
+ environment's required human approval and the selected-tag deployment rule in
373
+ [Public repository security](PUBLIC_SECURITY.md). That environment rule, with
374
+ the maintainer-only tag ruleset, is the trust boundary. The in-repo resolver is
375
+ defense in depth, not the trust boundary. Then prepare a
354
376
  new Code stable or RC version and annotated tag through the same PR and main CI process.
355
- Dispatch its tag with the matching channel, `mode=oidc` and
356
- `confirm_publication=true`. Stable versions require `latest` and a stable exact
377
+ Dispatch the workflow from that exact tag ref, with the matching channel, `mode=oidc` and
378
+ `confirm_publication=true`. A dispatch from `main` is rejected. Stable versions require `latest` and a stable exact
357
379
  Harness dependency; canonical RC versions require `next`. Version/channel drift
358
380
  fails before engine downloads. An RC may pin a stable engine. Stable publication
359
381
  does not upgrade beta/experimental APIs, provisional providers or Desktop alpha.
@@ -11,7 +11,7 @@ action on a fresh review ticket; model prose and a completed run are not accepta
11
11
 
12
12
  ## Operator flow
13
13
 
14
- 1. Open `zhivex-code web`, select an allowed workspace and create an empty session.
14
+ 1. Open `zhivex-code web`, open Sessions, choose an allowed project and create an empty session.
15
15
  Choose **Define task**. Enter the goal, exact existing editable files, package
16
16
  checks and explicit input/output/total token limits for the entire task.
17
17
  2. Review each proposed edit or check through the existing exact approval controls.
@@ -1,7 +1,7 @@
1
1
  # Engine API and Code integration
2
2
 
3
3
  The terminal-independent package entrypoints shipped in Harness 1.3.0 and remain
4
- in published Harness 1.4.0 (npm `latest`). This checkout is Harness 1.4.1, pending publication. They are not present in Harness 1.2.0.
4
+ in published Harness 1.4.0 (npm `latest`). This checkout is Harness 1.5.0, pending publication. They are not present in Harness 1.2.0.
5
5
  The protected workflow validated the exact stable tarball, live gates and
6
6
  registry integrity/provenance; local candidate smoke remains contributor evidence. Do not install registry 1.2.0 and
7
7
  expect the new entrypoints to exist.
@@ -81,14 +81,18 @@ Library callers can inject a custom `McpClient`. Trusted read-only annotations a
81
81
 
82
82
  ## MCP result boundary
83
83
 
84
- MCP descriptions and results are untrusted. The harness:
84
+ MCP descriptions and results are untrusted data, including text that looks like an identifier, digest, or configuration value. The harness:
85
85
 
86
86
  - bounds discovery pages, discovered tools, time, and response bytes;
87
87
  - validates declared input and structured-output schemas through the SDK MCP registry;
88
88
  - propagates abort signals, timeout intent, and idempotency keys;
89
89
  - rejects common prompt-injection directives before they enter model context;
90
90
  - records server permissions and risk in tool metadata;
91
- - promotes network, write, or external-side-effect calls to durable interrupt approval.
91
+ - promotes network, write, or external-side-effect calls to durable interrupt approval;
92
+ - removes exact secret values before a result is published;
93
+ - when a server was admitted with secret references, flags known encodings of those secrets and other high-entropy encoded tokens in tool results.
94
+
95
+ That flagging is optional and incomplete. Split, encrypted, or low-entropy encodings can still appear. Discovery schemas are rejected rather than rewritten when they contain an exact secret. Treat every MCP result as data, not as instructions and not as proof that a credential is absent.
92
96
 
93
97
  The injection filter is deliberately conservative and may reject legitimate security documentation containing imperative attack text. Use a separate trusted preprocessing boundary if that content must be analyzed; do not disable the harness boundary for an untrusted server.
94
98
 
package/docs/FIRST_USE.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # First use
2
2
 
3
- This guide targets stable `1.4.1`, pending publication to npm `latest`. The versioned install commands below become available only after protected publication. npm `latest` remains stable 1.3.0 until that publication.
3
+ This guide targets stable `1.5.0`, pending publication to npm `latest`. The versioned install commands below become available only after protected publication. npm `latest` remains stable 1.4.1 until that publication.
4
4
 
5
5
  ## Install and start
6
6
 
7
7
  Install Node.js 22.13.0 or newer and Git. Then:
8
8
 
9
9
  ```sh
10
- npm install -g @zhivex-ai/harness@1.4.1
10
+ npm install -g @zhivex-ai/harness@1.5.0
11
11
  cd /path/to/your/project
12
12
  zhx
13
13
  ```
@@ -82,16 +82,16 @@ For command execution, configure [OCI isolation](EXECUTION_ENVIRONMENTS.md).
82
82
  With Bun 1.4.0 or newer and a supported Node runtime:
83
83
 
84
84
  ```sh
85
- bun add --global @zhivex-ai/harness@1.4.1
85
+ bun add --global @zhivex-ai/harness@1.5.0
86
86
  zhx
87
87
  ```
88
88
 
89
89
  For a disposable version/help/environment check without a global installation:
90
90
 
91
91
  ```sh
92
- bunx @zhivex-ai/harness@1.4.1 --version
93
- bunx @zhivex-ai/harness@1.4.1 --help
94
- bunx @zhivex-ai/harness@1.4.1 doctor
92
+ bunx @zhivex-ai/harness@1.5.0 --version
93
+ bunx @zhivex-ai/harness@1.5.0 --help
94
+ bunx @zhivex-ai/harness@1.5.0 doctor
95
95
  ```
96
96
 
97
97
  For source development, use the [contributor guide](https://github.com/Zhivex/zhivex-harness/blob/main/CONTRIBUTING.md).