@gilbertgt/dsh-plan-orchestrator 1.0.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.
@@ -0,0 +1,101 @@
1
+ //#region src/orchestration/failover.ts
2
+ function eligibleTransportFailure(error) {
3
+ const failure = error;
4
+ const s = `${failure?.code ?? ""} ${failure?.failure?.code ?? ""} ${failure?.message ?? failure ?? ""}`.toLowerCase();
5
+ return /(timeout|rate.?limit|quota|transport|connection|network|unavailable|auth_failed|missing_credential|server|overload|503|502|504)/.test(s) && !/(cancel|abort|refusal|invalid.*schema|contract|policy|permission)/.test(s);
6
+ }
7
+ //#endregion
8
+ //#region src/planning/planner-route.ts
9
+ async function validateFixedRoute(llm, route) {
10
+ if (route.mode === "current") return {};
11
+ if (!route.provider || !route.model) throw new Error("fixed route needs provider and model");
12
+ const requested = {
13
+ provider: route.provider,
14
+ model: route.model,
15
+ ...route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {},
16
+ ...route.maxTokens ? { maxTokens: route.maxTokens } : {}
17
+ };
18
+ if (typeof llm?.resolveCallConfig !== "function") throw new Error("DSH llm.resolveCallConfig is unavailable");
19
+ await llm.resolveCallConfig(requested);
20
+ for (const fallback of route.fallbacks) await llm.resolveCallConfig(fallback);
21
+ return requested;
22
+ }
23
+ function routeAt(route, current, index) {
24
+ if (index === 0) {
25
+ if (route.mode === "current") {
26
+ if (!current?.provider || !current?.model) return void 0;
27
+ return {
28
+ provider: current.provider,
29
+ model: current.model,
30
+ reasoningEffort: current.reasoningEffort,
31
+ maxTokens: current.maxTokens
32
+ };
33
+ }
34
+ return {
35
+ provider: route.provider,
36
+ model: route.model,
37
+ reasoningEffort: route.reasoningEffort,
38
+ maxTokens: route.maxTokens
39
+ };
40
+ }
41
+ return route.fallbacks[index - 1];
42
+ }
43
+ function installPlannerRoute(ctx, getRoute, isPlanActive, isEnabled = () => true) {
44
+ const fallbackIndex = /* @__PURE__ */ new WeakMap();
45
+ const request = ctx.on("agent/request", async ({ agent }, next) => {
46
+ const current = await next();
47
+ if (!isEnabled(agent) || !isPlanActive(agent)) {
48
+ fallbackIndex.delete(agent);
49
+ return current;
50
+ }
51
+ const selected = routeAt(getRoute(agent), current, fallbackIndex.get(agent) ?? 0);
52
+ if (!selected) return current;
53
+ await ctx.llm.resolveCallConfig(selected);
54
+ return {
55
+ ...current,
56
+ provider: selected.provider,
57
+ model: selected.model,
58
+ reasoningEffort: selected.reasoningEffort,
59
+ maxTokens: selected.maxTokens ?? current.maxTokens
60
+ };
61
+ });
62
+ const assembly = ctx.on("system-prompt/assemble", async (assembled, context, next) => {
63
+ const result = await next();
64
+ const agent = context?.agent;
65
+ if (!agent || !isEnabled(agent) || !isPlanActive(agent)) return result;
66
+ const selected = routeAt(getRoute(agent), { ...agent.options }, fallbackIndex.get(agent) ?? 0);
67
+ if (!selected) return result;
68
+ return {
69
+ ...result,
70
+ variables: {
71
+ ...result.variables,
72
+ model: selected.model,
73
+ provider: selected.provider
74
+ }
75
+ };
76
+ });
77
+ const requestError = ctx.on("agent/request-error", async (payload, next) => {
78
+ const downstream = await next();
79
+ if (downstream !== void 0) return downstream;
80
+ const { agent, failure, signal } = payload;
81
+ if (signal?.aborted || !isEnabled(agent) || !isPlanActive(agent) || !eligibleTransportFailure(failure)) return void 0;
82
+ const route = getRoute(agent);
83
+ const nextIndex = (fallbackIndex.get(agent) ?? 0) + 1;
84
+ if (nextIndex > route.fallbacks.length) return void 0;
85
+ fallbackIndex.set(agent, nextIndex);
86
+ return { kind: "retry" };
87
+ });
88
+ const reset = ctx.on("agent/pre-step", async ({ agent }, next) => {
89
+ const decision = await next();
90
+ if (!isEnabled(agent) || !isPlanActive(agent)) fallbackIndex.delete(agent);
91
+ return decision;
92
+ });
93
+ return () => {
94
+ request?.();
95
+ assembly?.();
96
+ requestError?.();
97
+ reset?.();
98
+ };
99
+ }
100
+ //#endregion
101
+ export { validateFixedRoute as n, eligibleTransportFailure as r, installPlannerRoute as t };
@@ -0,0 +1,137 @@
1
+ import { C as git, S as fullHead, T as splitNul, a as openPrForBranch, f as assertOwnedPaths, h as atomicJson, i as ghRepository, l as assertTrustedReceipts, r as ghRaw, s as remotePr, u as receiptIndex } from "./github-Cy4Pm35G.js";
2
+ import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
4
+ //#region src/external/publication.ts
5
+ function assertPublishGate(gate) {
6
+ if (!gate.reviewPass || !gate.validationCurrent || !gate.ownershipPass) throw new Error("publication gate requires Reviewer PASS + current validation + ownership PASS");
7
+ }
8
+ function assertNoForce(args) {
9
+ if (args.some((arg) => arg === "--force" || arg === "-f" || arg.startsWith("--force-with-lease"))) throw new Error("force push forbidden");
10
+ }
11
+ function completionComment(meta, pr, head) {
12
+ const payload = { externalCompletion: {
13
+ version: 1,
14
+ revision: meta.revision,
15
+ repository: meta.repository,
16
+ issue: meta.issueNumber,
17
+ pr,
18
+ head
19
+ } };
20
+ return [
21
+ "IMPLEMENTATION COMPLETE",
22
+ "",
23
+ "```json",
24
+ JSON.stringify(payload, null, 2),
25
+ "```"
26
+ ].join("\n");
27
+ }
28
+ async function publishExternalRun(opts) {
29
+ await assertTrustedReceipts(opts.receipts, opts.expectedHead, opts.expectedOwnershipFingerprint);
30
+ if (await fullHead(opts.cwd) !== opts.expectedHead) throw new Error("stale HEAD before publication");
31
+ assertPublishGate({
32
+ reviewPass: true,
33
+ validationCurrent: true,
34
+ ownershipPass: true
35
+ });
36
+ if (opts.ownedPaths.length === 0) throw new Error("publication blocked: no run-owned changes");
37
+ await git(opts.cwd, [
38
+ "add",
39
+ "--",
40
+ ...opts.ownedPaths
41
+ ]);
42
+ const staged = splitNul((await git(opts.cwd, [
43
+ "diff",
44
+ "--cached",
45
+ "--name-only",
46
+ "-z",
47
+ "--"
48
+ ])).stdout);
49
+ if (staged.length === 0) throw new Error("publication blocked: no staged run-owned changes");
50
+ assertOwnedPaths(staged, opts.ownedPaths);
51
+ const diff = (await git(opts.cwd, [
52
+ "diff",
53
+ "--cached",
54
+ "--binary",
55
+ "--no-ext-diff",
56
+ "--"
57
+ ])).stdout;
58
+ const candidate = {
59
+ version: 1,
60
+ repository: opts.meta.repository,
61
+ issue: opts.meta.issueNumber,
62
+ revision: opts.meta.revision,
63
+ branch: opts.meta.branch,
64
+ expectedHead: opts.expectedHead,
65
+ staged,
66
+ stagedDiffSha256: createHash("sha256").update(diff).digest("hex"),
67
+ receipts: receiptIndex(opts.receipts),
68
+ at: (/* @__PURE__ */ new Date()).toISOString()
69
+ };
70
+ await atomicJson(join(opts.runDir, "external-issue.json"), { candidate });
71
+ await git(opts.cwd, [
72
+ "commit",
73
+ "-m",
74
+ `Implement Issue #${opts.meta.issueNumber} external plan r${opts.meta.revision}`
75
+ ]);
76
+ const commit = await fullHead(opts.cwd);
77
+ const pushArgs = [
78
+ "push",
79
+ "origin",
80
+ opts.meta.branch
81
+ ];
82
+ assertNoForce(pushArgs);
83
+ await git(opts.cwd, pushArgs);
84
+ let pr = await openPrForBranch(opts.cwd, opts.meta.branch);
85
+ if (!pr) {
86
+ const base = (await ghRepository(opts.cwd))?.defaultBranchRef?.name;
87
+ if (!base) throw new Error("default branch unavailable");
88
+ await ghRaw(opts.cwd, [
89
+ "pr",
90
+ "create",
91
+ "--base",
92
+ base,
93
+ "--head",
94
+ opts.meta.branch,
95
+ "--title",
96
+ `Issue #${opts.meta.issueNumber}: external plan r${opts.meta.revision}`,
97
+ "--body",
98
+ `Implements the trusted external PlanArtifact revision ${opts.meta.revision} for Issue #${opts.meta.issueNumber}.\n\nReviewer: PASS. Host validation receipts are recorded by dsh-plan-orchestrator.`
99
+ ]);
100
+ pr = await openPrForBranch(opts.cwd, opts.meta.branch);
101
+ }
102
+ if (!pr?.number) throw new Error("PR create/reuse failed");
103
+ const prNumber = Number(pr.number);
104
+ const remote = await remotePr(opts.cwd, prNumber);
105
+ if (remote?.headRefOid !== commit) throw new Error(`remote PR head mismatch: ${remote?.headRefOid} != ${commit}`);
106
+ const externalCompletion = {
107
+ version: 1,
108
+ revision: opts.meta.revision,
109
+ repository: opts.meta.repository,
110
+ issue: opts.meta.issueNumber,
111
+ pr: prNumber,
112
+ head: commit
113
+ };
114
+ await ghRaw(opts.cwd, [
115
+ "issue",
116
+ "comment",
117
+ String(opts.meta.issueNumber),
118
+ "--body",
119
+ completionComment(opts.meta, prNumber, commit)
120
+ ]);
121
+ await atomicJson(join(opts.runDir, "external-issue.json"), {
122
+ candidate,
123
+ completion: externalCompletion,
124
+ pr: {
125
+ number: prNumber,
126
+ url: remote?.url,
127
+ head: commit
128
+ }
129
+ });
130
+ return {
131
+ pr: prNumber,
132
+ head: commit,
133
+ url: remote?.url
134
+ };
135
+ }
136
+ //#endregion
137
+ export { publishExternalRun };
@@ -0,0 +1,131 @@
1
+ import { n as validateFixedRoute } from "./planner-route-B9z5GziX.js";
2
+ //#region src/contract/rpc.ts
3
+ const RPC_METHODS = [
4
+ "model-catalog",
5
+ "route-validate",
6
+ "run-list",
7
+ "run-detail",
8
+ "run-cancel",
9
+ "run-resume",
10
+ "run-cleanup",
11
+ "diagnostics",
12
+ "external-preflight"
13
+ ];
14
+ function assertRpcBody(value) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("request body must be a JSON object");
16
+ if (Buffer.byteLength(JSON.stringify(value), "utf8") > 256 * 1024) throw new Error("request body exceeds 256KiB");
17
+ return value;
18
+ }
19
+ function boundedId(value, name) {
20
+ if (typeof value !== "string" || value.length < 1 || value.length > 300 || value.includes("\0")) throw new Error(`${name} invalid`);
21
+ return value;
22
+ }
23
+ //#endregion
24
+ //#region src/model-catalog.ts
25
+ async function modelCatalog(ctx) {
26
+ const providers = ctx.llm.listProviders();
27
+ return Promise.all(providers.map(async (p) => {
28
+ let models = [];
29
+ let error;
30
+ try {
31
+ models = await ctx.llm.listModels(p.id);
32
+ } catch (e) {
33
+ error = e.message;
34
+ }
35
+ return {
36
+ provider: { ...p },
37
+ models,
38
+ error
39
+ };
40
+ }));
41
+ }
42
+ async function routeValidate(ctx, route) {
43
+ return validateFixedRoute(ctx.llm, route);
44
+ }
45
+ //#endregion
46
+ //#region src/rpc-server.ts
47
+ const MAX_RESPONSE = 2 * 1024 * 1024;
48
+ async function body(request) {
49
+ if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") throw new Error("application/json required");
50
+ return assertRpcBody(await request.json());
51
+ }
52
+ function safeError(error) {
53
+ return String(error?.message ?? error).replace(/[A-Za-z]:\\[^\s]+|\/(?:[^\s/]+\/){2,}[^\s]*/g, "<redacted-path>").slice(0, 1e3);
54
+ }
55
+ function response(value, status = 200) {
56
+ let json = JSON.stringify(value);
57
+ if (Buffer.byteLength(json, "utf8") > MAX_RESPONSE) {
58
+ json = JSON.stringify({
59
+ ok: false,
60
+ error: {
61
+ code: "PLAN_ORCHESTRATOR_RESPONSE_TOO_LARGE",
62
+ message: "response exceeds bounded API payload"
63
+ }
64
+ });
65
+ status = 413;
66
+ }
67
+ return new Response(json, {
68
+ status,
69
+ headers: { "content-type": "application/json" }
70
+ });
71
+ }
72
+ function registerRpc(connection, deps) {
73
+ const disposers = [];
74
+ for (const method of RPC_METHODS) disposers.push(connection.fetch.register({
75
+ path: `/api/plan-orchestrator/${method}`,
76
+ methods: ["POST"],
77
+ requestBody: "buffered",
78
+ async fetch(request) {
79
+ try {
80
+ const input = await body(request);
81
+ let data;
82
+ switch (method) {
83
+ case "model-catalog":
84
+ data = await modelCatalog(deps.ctx);
85
+ break;
86
+ case "route-validate":
87
+ data = await routeValidate(deps.ctx, input.route);
88
+ break;
89
+ case "run-list":
90
+ data = deps.runList?.({ sessionId: typeof input.sessionId === "string" ? boundedId(input.sessionId, "sessionId") : void 0 }) ?? [];
91
+ break;
92
+ case "run-detail":
93
+ data = await deps.runDetail?.({ runId: boundedId(input.runId, "runId") });
94
+ break;
95
+ case "run-cancel":
96
+ data = await deps.runCancel?.({ runId: boundedId(input.runId, "runId") });
97
+ break;
98
+ case "run-resume":
99
+ if (deps.isEnabled && !deps.isEnabled()) throw new Error("Plan Orchestrator is disabled in Settings → Plan Mode.");
100
+ if (deps.canResume && !deps.canResume()) throw new Error("Safe Resume is disabled in Settings → Plan Mode.");
101
+ data = await deps.runResume?.({ runId: boundedId(input.runId, "runId") });
102
+ break;
103
+ case "run-cleanup":
104
+ data = await deps.runCleanup?.({ runId: boundedId(input.runId, "runId") });
105
+ break;
106
+ case "diagnostics":
107
+ data = await deps.diagnostics?.(input);
108
+ break;
109
+ case "external-preflight":
110
+ data = await deps.externalPreflight?.(input);
111
+ break;
112
+ }
113
+ return response({
114
+ ok: true,
115
+ data
116
+ });
117
+ } catch (error) {
118
+ return response({
119
+ ok: false,
120
+ error: {
121
+ code: "PLAN_ORCHESTRATOR_RPC",
122
+ message: safeError(error)
123
+ }
124
+ }, 400);
125
+ }
126
+ }
127
+ }));
128
+ return () => disposers.forEach((dispose) => dispose?.());
129
+ }
130
+ //#endregion
131
+ export { registerRpc };
package/package.json ADDED
@@ -0,0 +1,144 @@
1
+ {
2
+ "name": "@gilbertgt/dsh-plan-orchestrator",
3
+ "version": "1.0.0",
4
+ "description": "Product-grade orchestration layer for DeepSeek Harness native Plan Mode",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./cordis.patch.yml": "./cordis.patch.yml",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "lib/*.js",
15
+ "cordis.patch.yml",
16
+ "compatibility.json",
17
+ "profiles/*.yml",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "engines": {
25
+ "node": "^22.19.0 || >=24.0.0"
26
+ },
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "inject": [
33
+ "@deepseek-ai/dsh-api-remotes",
34
+ "@deepseek-ai/dsh-client-connection",
35
+ "@deepseek-ai/dsh-client-locale",
36
+ "@deepseek-ai/dsh-client-ui-conversation",
37
+ "@deepseek-ai/dsh-client-ui-settings",
38
+ "@deepseek-ai/dsh-client-ui-slots"
39
+ ],
40
+ "platform": "web"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "test": "node --experimental-strip-types --test test/unit/*.test.mjs test/integration/*.test.mjs",
45
+ "test:unit": "node --experimental-strip-types --test test/unit/*.test.mjs",
46
+ "test:integration": "node --experimental-strip-types --test test/integration/*.test.mjs",
47
+ "test:platform": "node --experimental-strip-types --test test/platform/*.test.mjs test/integration/*.test.mjs",
48
+ "typecheck": "tsc --noEmit",
49
+ "build": "tsdown --config tsdown.config.mjs",
50
+ "pack:check": "node scripts/pack-check.mjs",
51
+ "compat:check": "node scripts/compatibility.mjs",
52
+ "check": "npm test && npm run typecheck && npm run build && npm run pack:check && npm run compat:check",
53
+ "e2e:rc1": "node scripts/e2e-rc1.mjs",
54
+ "prepack": "npm run build",
55
+ "prepublishOnly": "node scripts/direct-publish-guard.mjs",
56
+ "release:source": "node scripts/release-source-check.mjs",
57
+ "release:pack": "node scripts/release-pack.mjs"
58
+ },
59
+ "peerDependencies": {
60
+ "@deepseek-ai/cordis": "4.0.1 || 4.0.2",
61
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
62
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
63
+ "@deepseek-ai/dsh-plan-mode": "0.1.5-rc.1",
64
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
65
+ "@deepseek-ai/dsh-subagent": "0.1.5-rc.1",
66
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
67
+ "react": "^18.2.0",
68
+ "react-dom": "^18.2.0",
69
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.1",
70
+ "@deepseek-ai/dsh-sandbox": "0.1.5-rc.1",
71
+ "@deepseek-ai/dsh-session-projection": "0.1.5-rc.1",
72
+ "@deepseek-ai/dsh-session": "0.1.5-rc.1",
73
+ "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.1",
74
+ "@deepseek-ai/dsh-commands": "0.1.5-rc.1"
75
+ },
76
+ "optionalDependencies": {
77
+ "@deepseek-ai/dsh-sdk-client": "0.1.5-rc.1"
78
+ },
79
+ "devDependencies": {
80
+ "@deepseek-ai/cordis": "4.0.2",
81
+ "@deepseek-ai/dsh": "0.1.5-rc.1",
82
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
83
+ "@deepseek-ai/dsh-api-remotes": "0.1.5-rc.1",
84
+ "@deepseek-ai/dsh-attachment": "0.1.5-rc.1",
85
+ "@deepseek-ai/dsh-brand": "0.1.5-rc.1",
86
+ "@deepseek-ai/dsh-client-connection": "0.1.5-rc.1",
87
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.1",
88
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
89
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
90
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
91
+ "@deepseek-ai/dsh-code-runtime": "0.1.5-rc.1",
92
+ "@deepseek-ai/dsh-commands": "0.1.5-rc.1",
93
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.1",
94
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
95
+ "@deepseek-ai/dsh-plan-mode": "0.1.5-rc.1",
96
+ "@deepseek-ai/dsh-sandbox": "0.1.5-rc.1",
97
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.1",
98
+ "@deepseek-ai/dsh-scope": "0.1.5-rc.1",
99
+ "@deepseek-ai/dsh-sdk-client": "0.1.5-rc.1",
100
+ "@deepseek-ai/dsh-sdk-protocol": "0.1.5-rc.1",
101
+ "@deepseek-ai/dsh-session": "0.1.5-rc.1",
102
+ "@deepseek-ai/dsh-session-projection": "0.1.5-rc.1",
103
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
104
+ "@deepseek-ai/dsh-subagent": "0.1.5-rc.1",
105
+ "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.1",
106
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
107
+ "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.1",
108
+ "@deepseek-ai/dsh-user-approval": "0.1.5-rc.1",
109
+ "@deepseek-ai/dsh-user-questions": "0.1.5-rc.1",
110
+ "@deepseek-ai/dsh-util-values": "0.1.5-rc.1",
111
+ "@deepseek-ai/schemastery": "3.18.2",
112
+ "@types/node": "^22.20.0",
113
+ "@types/react": "^18.3.0",
114
+ "@types/react-dom": "^18.3.0",
115
+ "pnpm": "12.4.1",
116
+ "tsdown": "0.22.2",
117
+ "typescript": "^5.9.0"
118
+ },
119
+ "license": "MIT",
120
+ "repository": {
121
+ "type": "git",
122
+ "url": "git+https://github.com/gilbertgt/DSH-plan-mode.git"
123
+ },
124
+ "peerDependenciesMeta": {
125
+ "@deepseek-ai/dsh-commands": {
126
+ "optional": true
127
+ }
128
+ },
129
+ "dependencies": {
130
+ "zod": "^4.4.3"
131
+ },
132
+ "overrides": {
133
+ "@deepseek-ai/dsh-attachment": "0.1.5-rc.1",
134
+ "@deepseek-ai/dsh-brand": "0.1.5-rc.1",
135
+ "@deepseek-ai/dsh-code-runtime": "0.1.5-rc.1",
136
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.1",
137
+ "@deepseek-ai/dsh-scope": "0.1.5-rc.1",
138
+ "@deepseek-ai/dsh-sdk-protocol": "0.1.5-rc.1",
139
+ "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.1",
140
+ "@deepseek-ai/dsh-user-approval": "0.1.5-rc.1",
141
+ "@deepseek-ai/dsh-user-questions": "0.1.5-rc.1",
142
+ "@deepseek-ai/dsh-util-values": "0.1.5-rc.1"
143
+ }
144
+ }
@@ -0,0 +1,5 @@
1
+ # Overlay for a dedicated SDK Reviewer fallback. Reviewer work is strictly
2
+ # read-only; the host also scopes visible tools so this is defense in depth.
3
+ - id: sandbox-policy
4
+ config:
5
+ mode: read-only
@@ -0,0 +1,5 @@
1
+ # Overlay for worktree-isolated SDK Workers. The sdk profile already composes
2
+ # dsh-base; this file only tightens the per-session filesystem policy.
3
+ - id: sandbox-policy
4
+ config:
5
+ mode: workspace-write