@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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.
- package/README.md +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* On Windows, `chmod` only controls POSIX-style bits in the ACE list and does NOT remove
|
|
5
5
|
* inherited permissions from other users. Real per-user isolation requires icacls to:
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
6
|
+
* 1. Grant the current user full control (icacls path /grant:r "CURRENTUSER:(F)")
|
|
7
|
+
* 2. Disable inheritance (icacls path /inheritance:r)
|
|
8
|
+
* 3. Strip broad explicit grants by SID (Everyone, Users, Authenticated Users)
|
|
9
|
+
*
|
|
10
|
+
* Owner grant MUST precede `/inheritance:r`. That flag is destructive: it drops inherited
|
|
11
|
+
* ACEs immediately. If a later step times out after inheritance is removed but before an
|
|
12
|
+
* explicit owner ACE exists, the temp is left with a protected empty DACL — owned by the
|
|
13
|
+
* current user yet unreadable/ununlinkable until Full Control is restored (issue #596).
|
|
9
14
|
*
|
|
10
15
|
* On non-Windows platforms the helpers fall through to the caller's existing chmod-based
|
|
11
16
|
* behaviour: they return ok:true without invoking any external process.
|
|
@@ -19,6 +24,10 @@
|
|
|
19
24
|
* icacls cannot block OAuth logins or token refresh (field report: Kimi auth
|
|
20
25
|
* stuck behind ETIMEDOUT). Real EPERM/EACCES/exit-code failures still throw:
|
|
21
26
|
* availability never silently overrides confidentiality for those.
|
|
27
|
+
* hardenSecretPathAsync / hardenSecretDirAsync — same policy, async icacls
|
|
28
|
+
* runner so the event loop is not held for the child lifetime (#612).
|
|
29
|
+
* HardenOptions.timeoutMemoKey — optional destination-path key for the
|
|
30
|
+
* timeout memo (atomic writers mint unique temps; never a parent directory).
|
|
22
31
|
* hardenSecretDir — same contract for directories.
|
|
23
32
|
*/
|
|
24
33
|
|
|
@@ -37,6 +46,13 @@ export interface HardenResult {
|
|
|
37
46
|
|
|
38
47
|
export interface HardenOptions {
|
|
39
48
|
required: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Optional timeout-memo key distinct from `targetPath` (issue #612).
|
|
51
|
+
* Atomic writers mint a fresh `.tmp` path per write; keying the timeout cache by the
|
|
52
|
+
* final destination path prevents re-stalling the event loop on every subsequent temp.
|
|
53
|
+
* Must NOT be a parent directory — directory ACLs are not authoritative for new files.
|
|
54
|
+
*/
|
|
55
|
+
timeoutMemoKey?: string;
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
/**
|
|
@@ -67,6 +83,7 @@ export interface IcaclsResult {
|
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
type IcaclsRunner = (args: string[], timeoutMs: number) => IcaclsResult;
|
|
86
|
+
type AsyncIcaclsRunner = (args: string[], timeoutMs: number) => Promise<IcaclsResult>;
|
|
70
87
|
|
|
71
88
|
function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
|
|
72
89
|
// Bun.spawnSync with windowsHide: Node execFileSync has hung under the GUI/proxy even
|
|
@@ -86,7 +103,43 @@ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
|
|
|
86
103
|
};
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Async icacls runner (#612): yields the event loop while waiting for the child.
|
|
108
|
+
* Timeout provenance is recorded by our timer (async Subprocess has no exitedDueToTimeout);
|
|
109
|
+
* we still await process exit before classifying so settlement is confirmed.
|
|
110
|
+
*/
|
|
111
|
+
async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
|
|
112
|
+
const proc = Bun.spawn(["icacls.exe", ...args], {
|
|
113
|
+
stdin: "ignore",
|
|
114
|
+
stdout: "pipe",
|
|
115
|
+
stderr: "ignore",
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
});
|
|
118
|
+
let timedOutByUs = false;
|
|
119
|
+
const timer = setTimeout(() => {
|
|
120
|
+
timedOutByUs = true;
|
|
121
|
+
try { proc.kill(); } catch { /* already exited */ }
|
|
122
|
+
}, Math.max(1, timeoutMs));
|
|
123
|
+
let exitCode: number | null = null;
|
|
124
|
+
try {
|
|
125
|
+
exitCode = await proc.exited;
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
const stdout = proc.stdout
|
|
130
|
+
? await new Response(proc.stdout).text().catch(() => "")
|
|
131
|
+
: "";
|
|
132
|
+
const timedOut = timedOutByUs;
|
|
133
|
+
return {
|
|
134
|
+
success: !timedOut && exitCode === 0,
|
|
135
|
+
exitCode: timedOut ? null : exitCode,
|
|
136
|
+
timedOut,
|
|
137
|
+
stdout,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
89
141
|
let icaclsRunner: IcaclsRunner = defaultIcaclsRunner;
|
|
142
|
+
let asyncIcaclsRunner: AsyncIcaclsRunner = defaultAsyncIcaclsRunner;
|
|
90
143
|
let platformOverride: string | null = null;
|
|
91
144
|
let nowFn: () => number = Date.now;
|
|
92
145
|
|
|
@@ -95,6 +148,11 @@ export function setIcaclsRunnerForTests(runner: IcaclsRunner | null): void {
|
|
|
95
148
|
icaclsRunner = runner ?? defaultIcaclsRunner;
|
|
96
149
|
}
|
|
97
150
|
|
|
151
|
+
/** Test seam: replace the async icacls runner. Pass null to restore the default. */
|
|
152
|
+
export function setAsyncIcaclsRunnerForTests(runner: AsyncIcaclsRunner | null): void {
|
|
153
|
+
asyncIcaclsRunner = runner ?? defaultAsyncIcaclsRunner;
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
/** Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner. */
|
|
99
157
|
export function setPlatformForTests(value: string | null): void {
|
|
100
158
|
platformOverride = value;
|
|
@@ -141,8 +199,8 @@ function currentWindowsUser(): string | undefined {
|
|
|
141
199
|
|
|
142
200
|
/**
|
|
143
201
|
* Run icacls to harden a single file system entry.
|
|
144
|
-
*
|
|
145
|
-
*
|
|
202
|
+
* Order is intentional (issue #596): grant the owner ACE first so a later
|
|
203
|
+
* `/inheritance:r` or `/remove:g` timeout cannot strand a protected zero-ACE DACL.
|
|
146
204
|
*
|
|
147
205
|
* We do NOT use a shell string; all arguments are passed as an array so no
|
|
148
206
|
* shell injection is possible even for paths with unusual characters.
|
|
@@ -151,6 +209,10 @@ function currentWindowsUser(): string | undefined {
|
|
|
151
209
|
*/
|
|
152
210
|
const BROAD_SIDS = ["*S-1-1-0", "*S-1-5-11", "*S-1-5-32-545"] as const;
|
|
153
211
|
|
|
212
|
+
function grantAce(user: string, directory: boolean): string {
|
|
213
|
+
return directory ? `${user}:(OI)(CI)(F)` : `${user}:(F)`;
|
|
214
|
+
}
|
|
215
|
+
|
|
154
216
|
function runIcacls(targetPath: string, directory: boolean, deadline: number): void {
|
|
155
217
|
const user = currentWindowsUser();
|
|
156
218
|
if (!user) {
|
|
@@ -170,13 +232,19 @@ function runIcacls(targetPath: string, directory: boolean, deadline: number): vo
|
|
|
170
232
|
if (!result.success) throw icaclsError(step, result);
|
|
171
233
|
};
|
|
172
234
|
|
|
173
|
-
// Step 1:
|
|
235
|
+
// Step 1: grant current user full control BEFORE any destructive ACL change.
|
|
236
|
+
// If this fails, inheritance is untouched and the writer keeps inherited access.
|
|
237
|
+
runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(user, directory)]);
|
|
238
|
+
|
|
239
|
+
// Step 2: disable inheritance and remove inherited ACEs. The explicit owner ACE
|
|
240
|
+
// from step 1 survives this transition, so a later failure still leaves cleanup access.
|
|
174
241
|
runOrThrow("/inheritance:r", [targetPath, "/inheritance:r"]);
|
|
175
242
|
|
|
176
|
-
// Step
|
|
243
|
+
// Step 3: remove broad explicit grants using stable SIDs (not localized names).
|
|
177
244
|
// Missing ACEs can yield a non-zero exit; verify with locale-independent /findsid
|
|
178
245
|
// before accepting the failure as harmless — a swallowed real failure would leave
|
|
179
246
|
// Everyone/Users/Authenticated Users grants while reporting hardened.
|
|
247
|
+
// `/remove:g` cannot remove the explicit current-user ACE installed in step 1.
|
|
180
248
|
const removal = run("/remove:g", [targetPath, "/remove:g", ...BROAD_SIDS]);
|
|
181
249
|
if (!removal.success) {
|
|
182
250
|
if (removal.timedOut) throw icaclsError("/remove:g", removal);
|
|
@@ -191,10 +259,41 @@ function runIcacls(targetPath: string, directory: boolean, deadline: number): vo
|
|
|
191
259
|
}
|
|
192
260
|
}
|
|
193
261
|
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Async counterpart of runIcacls — same step order and timeout/error classification (#612). */
|
|
265
|
+
async function runIcaclsAsync(targetPath: string, directory: boolean, deadline: number): Promise<void> {
|
|
266
|
+
const user = currentWindowsUser();
|
|
267
|
+
if (!user) {
|
|
268
|
+
throw new Error("Cannot determine current Windows user for ACL hardening");
|
|
269
|
+
}
|
|
194
270
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
271
|
+
const run = async (step: string, args: string[]): Promise<IcaclsResult> => {
|
|
272
|
+
const remaining = deadline - nowFn();
|
|
273
|
+
if (remaining <= 0) {
|
|
274
|
+
throw icaclsError(step, { success: false, exitCode: null, timedOut: true, stdout: "" });
|
|
275
|
+
}
|
|
276
|
+
return asyncIcaclsRunner(args, remaining);
|
|
277
|
+
};
|
|
278
|
+
const runOrThrow = async (step: string, args: string[]): Promise<void> => {
|
|
279
|
+
const result = await run(step, args);
|
|
280
|
+
if (!result.success) throw icaclsError(step, result);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
await runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(user, directory)]);
|
|
284
|
+
await runOrThrow("/inheritance:r", [targetPath, "/inheritance:r"]);
|
|
285
|
+
|
|
286
|
+
const removal = await run("/remove:g", [targetPath, "/remove:g", ...BROAD_SIDS]);
|
|
287
|
+
if (!removal.success) {
|
|
288
|
+
if (removal.timedOut) throw icaclsError("/remove:g", removal);
|
|
289
|
+
for (const sid of BROAD_SIDS) {
|
|
290
|
+
const found = await run("/findsid", [targetPath, "/findsid", sid]);
|
|
291
|
+
if (!found.success) throw icaclsError("/findsid", found);
|
|
292
|
+
if (found.stdout.includes(targetPath)) {
|
|
293
|
+
throw icaclsError("/remove:g", removal);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
198
297
|
}
|
|
199
298
|
|
|
200
299
|
/**
|
|
@@ -246,6 +345,27 @@ function describeAclStateAfterTimeout(targetPath: string, deadline: number): str
|
|
|
246
345
|
}
|
|
247
346
|
}
|
|
248
347
|
|
|
348
|
+
async function describeAclStateAfterTimeoutAsync(targetPath: string, deadline: number): Promise<string> {
|
|
349
|
+
try {
|
|
350
|
+
for (const sid of BROAD_SIDS) {
|
|
351
|
+
const remaining = deadline - nowFn();
|
|
352
|
+
if (remaining <= 0) return "ACL state unverified (budget exhausted)";
|
|
353
|
+
const found = await asyncIcaclsRunner([targetPath, "/findsid", sid], remaining);
|
|
354
|
+
if (!found.success) return "ACL state unverified (probe failed)";
|
|
355
|
+
if (found.stdout.includes(targetPath)) return "broad ACL grants still present";
|
|
356
|
+
}
|
|
357
|
+
return "no broad ACL grants detected (hardening still incomplete)";
|
|
358
|
+
} catch {
|
|
359
|
+
return "ACL state unverified (probe failed)";
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function timeoutMemoKey(targetPath: string, opts: HardenOptions): string {
|
|
364
|
+
// Destination-path memo only (issue #612). Never a parent directory — directory ACLs
|
|
365
|
+
// are not authoritative for newly created temps.
|
|
366
|
+
return opts.timeoutMemoKey ?? targetPath;
|
|
367
|
+
}
|
|
368
|
+
|
|
249
369
|
/**
|
|
250
370
|
* Shared harden flow for files and directories: one total budget (env-configurable)
|
|
251
371
|
* covering the initial attempt, ONE timeout retry, and the diagnostic verification.
|
|
@@ -261,7 +381,8 @@ function hardenEntry(
|
|
|
261
381
|
if (!existsSync(targetPath)) return { ok: true };
|
|
262
382
|
if (effectivePlatform() !== "win32") return { ok: true };
|
|
263
383
|
if (cache.has(targetPath)) return { ok: true };
|
|
264
|
-
|
|
384
|
+
const memoKey = timeoutMemoKey(targetPath, opts);
|
|
385
|
+
if (timedOutPaths.has(memoKey)) {
|
|
265
386
|
return { ok: false, diagnostics: "ACL hardening skipped — previous attempt timed out" };
|
|
266
387
|
}
|
|
267
388
|
|
|
@@ -281,7 +402,7 @@ function hardenEntry(
|
|
|
281
402
|
|
|
282
403
|
const diagnostics = sanitizeDiagnostics(lastErr);
|
|
283
404
|
if (isTimeoutError(lastErr)) {
|
|
284
|
-
timedOutPaths.add(
|
|
405
|
+
timedOutPaths.add(memoKey);
|
|
285
406
|
const state = describeAclStateAfterTimeout(targetPath, deadline);
|
|
286
407
|
const annotated = `${diagnostics}; ${state}`;
|
|
287
408
|
// Timeout-only soft-fail: a hung icacls must not block OAuth/token writes.
|
|
@@ -293,6 +414,47 @@ function hardenEntry(
|
|
|
293
414
|
return { ok: false, diagnostics };
|
|
294
415
|
}
|
|
295
416
|
|
|
417
|
+
/** Async counterpart of hardenEntry — yields while waiting on icacls (#612). */
|
|
418
|
+
async function hardenEntryAsync(
|
|
419
|
+
targetPath: string,
|
|
420
|
+
directory: boolean,
|
|
421
|
+
opts: HardenOptions,
|
|
422
|
+
cache: Set<string>,
|
|
423
|
+
): Promise<HardenResult> {
|
|
424
|
+
if (!existsSync(targetPath)) return { ok: true };
|
|
425
|
+
if (effectivePlatform() !== "win32") return { ok: true };
|
|
426
|
+
if (cache.has(targetPath)) return { ok: true };
|
|
427
|
+
const memoKey = timeoutMemoKey(targetPath, opts);
|
|
428
|
+
if (timedOutPaths.has(memoKey)) {
|
|
429
|
+
return { ok: false, diagnostics: "ACL hardening skipped — previous attempt timed out" };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const deadline = nowFn() + resolveHardenDeadlineMs();
|
|
433
|
+
let lastErr: unknown;
|
|
434
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
435
|
+
if (attempt > 0 && deadline - nowFn() <= 0) break;
|
|
436
|
+
try {
|
|
437
|
+
await runIcaclsAsync(targetPath, directory, deadline);
|
|
438
|
+
cache.add(targetPath);
|
|
439
|
+
return { ok: true };
|
|
440
|
+
} catch (err) {
|
|
441
|
+
lastErr = err;
|
|
442
|
+
if (!isTimeoutError(err)) break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const diagnostics = sanitizeDiagnostics(lastErr);
|
|
447
|
+
if (isTimeoutError(lastErr)) {
|
|
448
|
+
timedOutPaths.add(memoKey);
|
|
449
|
+
const state = await describeAclStateAfterTimeoutAsync(targetPath, deadline);
|
|
450
|
+
const annotated = `${diagnostics}; ${state}`;
|
|
451
|
+
console.warn(`[opencodex] ${annotated} — continuing without NTFS ACL harden`);
|
|
452
|
+
return { ok: false, diagnostics: annotated };
|
|
453
|
+
}
|
|
454
|
+
if (opts.required) throw new Error(diagnostics);
|
|
455
|
+
return { ok: false, diagnostics };
|
|
456
|
+
}
|
|
457
|
+
|
|
296
458
|
/**
|
|
297
459
|
* Harden a single file path with per-user NTFS ACLs on Windows.
|
|
298
460
|
* On non-Windows platforms, returns ok:true immediately (caller owns chmod).
|
|
@@ -304,6 +466,14 @@ export function hardenSecretPath(targetPath: string, opts: HardenOptions): Harde
|
|
|
304
466
|
return hardenEntry(targetPath, false, opts, hardenedPaths);
|
|
305
467
|
}
|
|
306
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Async harden for write paths that must not block the event loop (#612).
|
|
471
|
+
* Same success/timeout/error policy as hardenSecretPath.
|
|
472
|
+
*/
|
|
473
|
+
export function hardenSecretPathAsync(targetPath: string, opts: HardenOptions): Promise<HardenResult> {
|
|
474
|
+
return hardenEntryAsync(targetPath, false, opts, hardenedPaths);
|
|
475
|
+
}
|
|
476
|
+
|
|
307
477
|
/**
|
|
308
478
|
* Harden a directory path with per-user NTFS ACLs on Windows.
|
|
309
479
|
* On non-Windows platforms, returns ok:true immediately (caller owns chmod).
|
|
@@ -314,3 +484,10 @@ export function hardenSecretPath(targetPath: string, opts: HardenOptions): Harde
|
|
|
314
484
|
export function hardenSecretDir(targetPath: string, opts: HardenOptions): HardenResult {
|
|
315
485
|
return hardenEntry(targetPath, true, opts, hardenedDirectories);
|
|
316
486
|
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Async directory harden (#612). Same policy as hardenSecretDir.
|
|
490
|
+
*/
|
|
491
|
+
export function hardenSecretDirAsync(targetPath: string, opts: HardenOptions): Promise<HardenResult> {
|
|
492
|
+
return hardenEntryAsync(targetPath, true, opts, hardenedDirectories);
|
|
493
|
+
}
|
package/src/lib/winsw.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
|
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
21
|
import { expandUserPath, getConfigDir, loadConfig } from "../config";
|
|
22
|
+
import { recordOwnedConfigPath } from "./config-ownership";
|
|
22
23
|
import { durableBunPath } from "./bun-runtime";
|
|
23
24
|
import { serviceApiTokenFilePath } from "./service-secrets";
|
|
24
25
|
|
|
@@ -133,6 +134,7 @@ export async function ensureWinswBinary(fetchImpl: typeof fetch = fetch): Promis
|
|
|
133
134
|
unlinkSync(exe);
|
|
134
135
|
console.warn("⚠️ Existing WinSW binary failed hash verification; re-downloading.");
|
|
135
136
|
}
|
|
137
|
+
recordOwnedConfigPath(getConfigDir(), winswDir());
|
|
136
138
|
if (!existsSync(winswDir())) mkdirSync(winswDir(), { recursive: true });
|
|
137
139
|
let body: ArrayBuffer;
|
|
138
140
|
try {
|