@sweny-ai/core 0.1.145 → 0.1.147
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.
|
@@ -175,7 +175,8 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
|
|
|
175
175
|
* is a read-only run; `policy.deny` classes become native `disallowedTools`
|
|
176
176
|
* names, merged with `nativeDeny` and the legacy `disallowedTools`; and
|
|
177
177
|
* `policy.strict` makes MCP exclusive (`strictMcpConfig`) even for a
|
|
178
|
-
* write-capable node.
|
|
178
|
+
* write-capable node. Per-request sandbox mode overrides the client default,
|
|
179
|
+
* and policy egress supplies the node hosts without dropping scoped env access.
|
|
179
180
|
*/
|
|
180
181
|
run(req: HarnessRunRequest): Promise<HarnessRunResult>;
|
|
181
182
|
private runQuery;
|
|
@@ -272,7 +272,8 @@ export class ClaudeCodeHarness {
|
|
|
272
272
|
* is a read-only run; `policy.deny` classes become native `disallowedTools`
|
|
273
273
|
* names, merged with `nativeDeny` and the legacy `disallowedTools`; and
|
|
274
274
|
* `policy.strict` makes MCP exclusive (`strictMcpConfig`) even for a
|
|
275
|
-
* write-capable node.
|
|
275
|
+
* write-capable node. Per-request sandbox mode overrides the client default,
|
|
276
|
+
* and policy egress supplies the node hosts without dropping scoped env access.
|
|
276
277
|
*/
|
|
277
278
|
async run(req) {
|
|
278
279
|
const policy = req.policy ?? {
|
|
@@ -301,6 +302,8 @@ export class ClaudeCodeHarness {
|
|
|
301
302
|
readOnly,
|
|
302
303
|
disallowedTools: disallowedTools.length > 0 ? disallowedTools : undefined,
|
|
303
304
|
strictMcp: readOnly || policy.strict,
|
|
305
|
+
sandboxMode: policy.sandbox,
|
|
306
|
+
agentAccess: { envVars: req.agentAccess?.envVars ?? [], domains: policy.egress },
|
|
304
307
|
});
|
|
305
308
|
return { ...result, harness: this.info(), degraded: gate.degraded };
|
|
306
309
|
}
|
|
@@ -318,7 +321,7 @@ export class ClaudeCodeHarness {
|
|
|
318
321
|
// `strict` fails the node here instead.
|
|
319
322
|
const sandbox = resolveAgentSandbox({
|
|
320
323
|
env: process.env,
|
|
321
|
-
mode: this.sandboxMode,
|
|
324
|
+
mode: opts.sandboxMode ?? this.sandboxMode,
|
|
322
325
|
allowedDomains: this.sandboxAllowedDomains,
|
|
323
326
|
nodeDomains: agentAccess?.domains,
|
|
324
327
|
probe: this.sandboxProbe,
|
|
@@ -326,7 +329,7 @@ export class ClaudeCodeHarness {
|
|
|
326
329
|
});
|
|
327
330
|
if (sandbox.error) {
|
|
328
331
|
this.logger.error(sandbox.error);
|
|
329
|
-
return { status: "failed", data: { error: sandbox.error }, toolCalls: [] };
|
|
332
|
+
return { status: "failed", data: { error: sandbox.error, refused: true }, toolCalls: [] };
|
|
330
333
|
}
|
|
331
334
|
if (sandbox.warning && !this.sandboxWarned) {
|
|
332
335
|
this.sandboxWarned = true;
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Every sweny opinion in the Codex column of the enforcement matrix is
|
|
22
22
|
* enforced natively here, kept by sweny (watchdog, fencing, env scoping,
|
|
23
|
-
* output
|
|
23
|
+
* output parsing), reported in `degraded`, or refused under strict policy
|
|
24
24
|
* by `policyGate`. Nothing is dropped silently.
|
|
25
25
|
*/
|
|
26
26
|
import type { Logger, McpServerConfig, ToolContext } from "../types.js";
|
package/dist/harness/codex.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Every sweny opinion in the Codex column of the enforcement matrix is
|
|
22
22
|
* enforced natively here, kept by sweny (watchdog, fencing, env scoping,
|
|
23
|
-
* output
|
|
23
|
+
* output parsing), reported in `degraded`, or refused under strict policy
|
|
24
24
|
* by `policyGate`. Nothing is dropped silently.
|
|
25
25
|
*/
|
|
26
26
|
import { spawn, execFile } from "node:child_process";
|
|
@@ -201,15 +201,21 @@ export function isStrictCompatibleSchema(schema) {
|
|
|
201
201
|
// ─── Process registry (crash cleanup) ────────────────────────────
|
|
202
202
|
const liveChildren = new Set();
|
|
203
203
|
let exitHookInstalled = false;
|
|
204
|
-
/**
|
|
205
|
-
* Stop Codex. It stays in sweny's process group (not detached), so a Ctrl-C
|
|
206
|
-
* or a CI cancel reaches it too; its MCP servers exit when their stdin closes.
|
|
207
|
-
*/
|
|
204
|
+
/** Stop the owned process tree, even after its leader has exited. */
|
|
208
205
|
function stopChild(child, signal) {
|
|
209
|
-
if (child.pid === undefined
|
|
206
|
+
if (child.pid === undefined)
|
|
210
207
|
return;
|
|
211
208
|
try {
|
|
212
|
-
|
|
209
|
+
if (process.platform !== "win32") {
|
|
210
|
+
// exec() gives each run its own process group. Descendants can retain
|
|
211
|
+
// stdout/stderr after the leader exits, so do not gate this on exitCode.
|
|
212
|
+
process.kill(-child.pid, signal);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Windows has no POSIX process groups. Bound the tree-kill utility too.
|
|
216
|
+
execFile("taskkill", ["/pid", String(child.pid), "/T", "/F"], { timeout: 1000 }, () => { });
|
|
217
|
+
child.kill(signal);
|
|
218
|
+
}
|
|
213
219
|
}
|
|
214
220
|
catch {
|
|
215
221
|
// already gone
|
|
@@ -220,10 +226,23 @@ function trackChild(child) {
|
|
|
220
226
|
if (!exitHookInstalled) {
|
|
221
227
|
exitHookInstalled = true;
|
|
222
228
|
// A crashing sweny must not leave Codex (and its MCP servers) running.
|
|
223
|
-
|
|
229
|
+
const cleanup = () => {
|
|
224
230
|
for (const c of liveChildren)
|
|
225
231
|
stopChild(c, "SIGKILL");
|
|
226
|
-
}
|
|
232
|
+
};
|
|
233
|
+
process.on("exit", cleanup);
|
|
234
|
+
// Detached POSIX groups no longer receive our terminal's signals. Clean
|
|
235
|
+
// them up before host once-handlers remove themselves during dispatch.
|
|
236
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
237
|
+
const onSignal = () => {
|
|
238
|
+
cleanup();
|
|
239
|
+
if (process.listenerCount(signal) === 1) {
|
|
240
|
+
process.removeListener(signal, onSignal);
|
|
241
|
+
process.kill(process.pid, signal);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
process.prependListener(signal, onSignal);
|
|
245
|
+
}
|
|
227
246
|
}
|
|
228
247
|
}
|
|
229
248
|
function mapUsage(u) {
|
|
@@ -470,6 +489,7 @@ export class CodexHarness {
|
|
|
470
489
|
cwd: opts.spawn.cwd,
|
|
471
490
|
env: opts.spawn.env,
|
|
472
491
|
stdio: ["pipe", "pipe", "pipe"],
|
|
492
|
+
detached: process.platform !== "win32",
|
|
473
493
|
});
|
|
474
494
|
}
|
|
475
495
|
catch (err) {
|
|
@@ -478,7 +498,9 @@ export class CodexHarness {
|
|
|
478
498
|
return out;
|
|
479
499
|
}
|
|
480
500
|
trackChild(child);
|
|
501
|
+
let finish;
|
|
481
502
|
const closed = new Promise((resolve) => {
|
|
503
|
+
finish = resolve;
|
|
482
504
|
child.once("close", (code, signal) => {
|
|
483
505
|
out.exitCode = code;
|
|
484
506
|
out.exitSignal = signal;
|
|
@@ -495,9 +517,24 @@ export class CodexHarness {
|
|
|
495
517
|
return;
|
|
496
518
|
stopped = true;
|
|
497
519
|
stopChild(child, "SIGTERM");
|
|
498
|
-
killTimer = setTimeout(() =>
|
|
520
|
+
killTimer = setTimeout(() => {
|
|
521
|
+
stopChild(child, "SIGKILL");
|
|
522
|
+
// An escaped descendant or failed OS kill must not retain this await
|
|
523
|
+
// through inherited pipes after cancellation or leader exit.
|
|
524
|
+
child.stdin?.destroy();
|
|
525
|
+
child.stdout?.destroy();
|
|
526
|
+
child.stderr?.destroy();
|
|
527
|
+
child.unref();
|
|
528
|
+
finish();
|
|
529
|
+
}, this.killGraceMs);
|
|
499
530
|
killTimer.unref?.();
|
|
500
531
|
};
|
|
532
|
+
child.once("exit", (code, signal) => {
|
|
533
|
+
out.exitCode = code;
|
|
534
|
+
out.exitSignal = signal;
|
|
535
|
+
// The leader is done, but its descendants may still own our pipes.
|
|
536
|
+
stop();
|
|
537
|
+
});
|
|
501
538
|
if (abort) {
|
|
502
539
|
const onAbort = () => {
|
|
503
540
|
out.abortReason = abort.reason();
|
|
@@ -649,6 +686,7 @@ export class CodexHarness {
|
|
|
649
686
|
onLine(buf);
|
|
650
687
|
if (killTimer)
|
|
651
688
|
clearTimeout(killTimer);
|
|
689
|
+
stopChild(child, "SIGKILL");
|
|
652
690
|
abort?.clear();
|
|
653
691
|
liveChildren.delete(child);
|
|
654
692
|
return out;
|
|
@@ -677,11 +715,11 @@ export class CodexHarness {
|
|
|
677
715
|
harness: this.info(),
|
|
678
716
|
degraded: [...degraded, ...extra],
|
|
679
717
|
});
|
|
680
|
-
const refused = (why) => {
|
|
718
|
+
const refused = (why, extra = []) => {
|
|
681
719
|
const msg = `codex refused this node: ${why}`;
|
|
682
720
|
this.logger.error(msg);
|
|
683
721
|
// `refused` keeps fail_soft from softening a policy refusal (executor.ts).
|
|
684
|
-
return tag({ status: "failed", data: { error: msg, refused: true }, toolCalls: [] });
|
|
722
|
+
return tag({ status: "failed", data: { error: msg, refused: true }, toolCalls: [] }, extra);
|
|
685
723
|
};
|
|
686
724
|
if (gate.refuse)
|
|
687
725
|
return refused(gate.refuse);
|
|
@@ -799,7 +837,7 @@ export class CodexHarness {
|
|
|
799
837
|
return refused(out.refuse);
|
|
800
838
|
// OpenAI strict structured outputs reject some JSON schemas. When Codex
|
|
801
839
|
// fails on the schema before doing anything, run once more without the
|
|
802
|
-
// native schema
|
|
840
|
+
// native schema in relaxed mode. Prompt-only output is not schema-validated.
|
|
803
841
|
if (schemaArgs.length > 0 &&
|
|
804
842
|
!out.completed &&
|
|
805
843
|
!out.abortReason &&
|
|
@@ -808,7 +846,10 @@ export class CodexHarness {
|
|
|
808
846
|
const why = isStrictCompatibleSchema(req.outputSchema)
|
|
809
847
|
? "codex rejected the output schema"
|
|
810
848
|
: "the output schema is not OpenAI strict-compatible";
|
|
811
|
-
extraDegraded.push(`structured_output: ${why};
|
|
849
|
+
extraDegraded.push(`structured_output: ${why}; native schema validation unavailable`);
|
|
850
|
+
if (policy.strict) {
|
|
851
|
+
return refused("strict policy requires structured_output validation; refusing prompt-only fallback", extraDegraded);
|
|
852
|
+
}
|
|
812
853
|
this.logger.warn(` codex: ${why}; retrying without --output-schema`);
|
|
813
854
|
const again = await execOnce(false);
|
|
814
855
|
if ("refuse" in again)
|
package/dist/safe-outputs.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function safeOutputsInstruction(declarations: SafeOutputDeclarati
|
|
|
47
47
|
export interface ActorInfo {
|
|
48
48
|
/** GitHub login of whoever triggered the run. */
|
|
49
49
|
login?: string;
|
|
50
|
-
/**
|
|
50
|
+
/** Association verified for this login, or explicitly supplied by the trusted caller. */
|
|
51
51
|
association?: string;
|
|
52
52
|
}
|
|
53
53
|
/** Who triggered this run: an explicit override, else GitHub Actions' env and event payload. */
|
package/dist/safe-outputs.js
CHANGED
|
@@ -129,13 +129,16 @@ export function safeOutputsInstruction(declarations) {
|
|
|
129
129
|
"Requests outside these types or limits are refused. Do not claim a write happened; say what you requested.",
|
|
130
130
|
].join("\n");
|
|
131
131
|
}
|
|
132
|
-
function eventAssociation(path) {
|
|
133
|
-
if (!path)
|
|
132
|
+
function eventAssociation(path, login) {
|
|
133
|
+
if (!path || !login)
|
|
134
134
|
return undefined;
|
|
135
135
|
try {
|
|
136
136
|
const ev = JSON.parse(readFileSync(path, "utf-8"));
|
|
137
137
|
for (const key of ["comment", "review", "issue", "pull_request"]) {
|
|
138
|
-
const
|
|
138
|
+
const author = ev?.[key];
|
|
139
|
+
if (typeof author?.user?.login !== "string" || author.user.login.toLowerCase() !== login.toLowerCase())
|
|
140
|
+
continue;
|
|
141
|
+
const a = author.author_association;
|
|
139
142
|
if (typeof a === "string" && a.length > 0)
|
|
140
143
|
return a;
|
|
141
144
|
}
|
|
@@ -148,7 +151,7 @@ function eventAssociation(path) {
|
|
|
148
151
|
/** Who triggered this run: an explicit override, else GitHub Actions' env and event payload. */
|
|
149
152
|
export function resolveActor(env, override) {
|
|
150
153
|
const login = override?.login ?? env.GITHUB_ACTOR ?? undefined;
|
|
151
|
-
const association = override?.association ?? eventAssociation(env.GITHUB_EVENT_PATH);
|
|
154
|
+
const association = override?.association ?? eventAssociation(env.GITHUB_EVENT_PATH, login);
|
|
152
155
|
return { ...(login ? { login } : {}), ...(association ? { association } : {}) };
|
|
153
156
|
}
|
|
154
157
|
/** True when no trust list is declared, or the actor matches one. Unknown actor = untrusted. */
|
|
@@ -356,11 +359,37 @@ export async function applySafeOutputs(o) {
|
|
|
356
359
|
refuse("title too long", at);
|
|
357
360
|
continue;
|
|
358
361
|
}
|
|
362
|
+
// A Linear comment's issue ID does not encode or enforce its declared team.
|
|
363
|
+
// Resolve it through the configured read tool and carry its immutable ID to
|
|
364
|
+
// the write. Missing lookup support or unverifiable membership fails closed.
|
|
365
|
+
let number = intent.number;
|
|
366
|
+
if (via === "linear" && type === "comment" && target) {
|
|
367
|
+
const lookup = o.skills.get(via)?.tools.find((t) => t.name === "linear_get_issue" && t.access === "read");
|
|
368
|
+
let issue;
|
|
369
|
+
try {
|
|
370
|
+
const result = await lookup?.handler({ id: number }, { config: o.config, logger: o.logger });
|
|
371
|
+
if (result && typeof result === "object") {
|
|
372
|
+
issue = result.issue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// A lookup error cannot authorize a write.
|
|
377
|
+
}
|
|
378
|
+
if (typeof issue?.id !== "string" || !issue.id || typeof issue.team?.id !== "string") {
|
|
379
|
+
refuse("cannot verify Linear issue team", at);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (issue.team.id.toLowerCase() !== target.toLowerCase()) {
|
|
383
|
+
refuse("issue outside the declared team", at);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
number = issue.id;
|
|
387
|
+
}
|
|
359
388
|
const write = {
|
|
360
389
|
type,
|
|
361
390
|
via,
|
|
362
391
|
...(target ? { target } : {}),
|
|
363
|
-
...(
|
|
392
|
+
...(number !== undefined ? { number } : {}),
|
|
364
393
|
...(title ? { title } : {}),
|
|
365
394
|
...(type !== "label" ? { body } : {}),
|
|
366
395
|
...(labels ? { labels } : {}),
|
package/dist/skills/linear.js
CHANGED