pi-landstrip 0.19.1 → 0.19.3
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/dist/index.ts +188 -158
- package/dist/worker-auth-entry.js +4 -4
- package/package.json +5 -5
package/dist/index.ts
CHANGED
|
@@ -352,16 +352,16 @@ class AsyncQueue {
|
|
|
352
352
|
const generation = this.generation;
|
|
353
353
|
let release;
|
|
354
354
|
const previous = this.tail;
|
|
355
|
-
this.tail = new Promise((
|
|
356
|
-
release =
|
|
355
|
+
this.tail = new Promise((resolve) => {
|
|
356
|
+
release = resolve;
|
|
357
357
|
});
|
|
358
358
|
const finish = () => {
|
|
359
359
|
release?.();
|
|
360
360
|
};
|
|
361
361
|
let wakeReset;
|
|
362
|
-
const resetSignal = new Promise((
|
|
363
|
-
wakeReset =
|
|
364
|
-
this.resetWaiters.push(
|
|
362
|
+
const resetSignal = new Promise((resolve) => {
|
|
363
|
+
wakeReset = resolve;
|
|
364
|
+
this.resetWaiters.push(resolve);
|
|
365
365
|
});
|
|
366
366
|
const dropResetWaiter = () => {
|
|
367
367
|
if (!wakeReset)
|
|
@@ -1396,15 +1396,15 @@ async function deleteProjectAgent(cwd, agent, agentDir = getAgentDir3()) {
|
|
|
1396
1396
|
function prepareProjectAgentEditor(cwd, agent, agentDir = getAgentDir3()) {
|
|
1397
1397
|
const origin = agent.origin;
|
|
1398
1398
|
if (origin?.path && origin.kind === "pi-markdown") {
|
|
1399
|
-
const
|
|
1399
|
+
const target = projectMarkdownPath(cwd, agentDir, agent);
|
|
1400
1400
|
return {
|
|
1401
1401
|
title: `Edit @${agent.name}`,
|
|
1402
1402
|
content: readFileSync4(origin.path, "utf8"),
|
|
1403
1403
|
async save(content) {
|
|
1404
1404
|
validateAgentRaw(agent.name, parseMarkdownAgentRaw(content));
|
|
1405
|
-
await withFileMutationQueue2(
|
|
1406
|
-
mkdirSync2(dirname2(
|
|
1407
|
-
writeFileSync2(
|
|
1405
|
+
await withFileMutationQueue2(target, async () => {
|
|
1406
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
1407
|
+
writeFileSync2(target, ensureFinalNewline2(content), { mode: 384 });
|
|
1408
1408
|
});
|
|
1409
1409
|
}
|
|
1410
1410
|
};
|
|
@@ -1629,10 +1629,10 @@ function getLandstripArgumentCompletions(argumentPrefix, runtime, contextProvide
|
|
|
1629
1629
|
const deps = { runtime, ctx: contextProvider?.() };
|
|
1630
1630
|
if (argv.words.length === 0 && !argv.trailing) {
|
|
1631
1631
|
const roots = [
|
|
1632
|
-
...SUBCOMMANDS.map((
|
|
1633
|
-
value:
|
|
1634
|
-
label:
|
|
1635
|
-
description:
|
|
1632
|
+
...SUBCOMMANDS.map((command) => ({
|
|
1633
|
+
value: command.name,
|
|
1634
|
+
label: command.name,
|
|
1635
|
+
description: command.summary
|
|
1636
1636
|
})),
|
|
1637
1637
|
...taskItems(runtime?.getTasks() ?? [], "")
|
|
1638
1638
|
];
|
|
@@ -1685,7 +1685,7 @@ function defaultSpawn(command, args, options) {
|
|
|
1685
1685
|
return spawn(command, [...args], options);
|
|
1686
1686
|
}
|
|
1687
1687
|
function delay(milliseconds) {
|
|
1688
|
-
return new Promise((
|
|
1688
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1689
1689
|
}
|
|
1690
1690
|
|
|
1691
1691
|
class RpcProcess {
|
|
@@ -1742,7 +1742,7 @@ class RpcProcess {
|
|
|
1742
1742
|
});
|
|
1743
1743
|
child.stdin.on("error", (error) => this.handleChildError(child, error, "RPC stdin error"));
|
|
1744
1744
|
child.on("error", (error) => this.handleChildError(child, error, "RPC process error"));
|
|
1745
|
-
this.exitPromise = new Promise((
|
|
1745
|
+
this.exitPromise = new Promise((resolve) => {
|
|
1746
1746
|
child.once("exit", (code, signal) => {
|
|
1747
1747
|
this.disposeAuthPipe?.();
|
|
1748
1748
|
this.disposeAuthPipe = undefined;
|
|
@@ -1750,13 +1750,13 @@ class RpcProcess {
|
|
|
1750
1750
|
this.child = null;
|
|
1751
1751
|
this.failAll(new Error(this.exitMessage(code, signal)));
|
|
1752
1752
|
}
|
|
1753
|
-
|
|
1753
|
+
resolve();
|
|
1754
1754
|
});
|
|
1755
1755
|
});
|
|
1756
|
-
await new Promise((
|
|
1756
|
+
await new Promise((resolve, reject) => {
|
|
1757
1757
|
const onSpawn = () => {
|
|
1758
1758
|
cleanup();
|
|
1759
|
-
|
|
1759
|
+
resolve();
|
|
1760
1760
|
};
|
|
1761
1761
|
const onError = (error) => {
|
|
1762
1762
|
cleanup();
|
|
@@ -1815,12 +1815,12 @@ class RpcProcess {
|
|
|
1815
1815
|
if (this.fatalError)
|
|
1816
1816
|
throw this.fatalError;
|
|
1817
1817
|
const id = `rpc-${++this.nextRequestId}`;
|
|
1818
|
-
const response = await new Promise((
|
|
1818
|
+
const response = await new Promise((resolve, reject) => {
|
|
1819
1819
|
const timer = setTimeout(() => {
|
|
1820
1820
|
this.pending.delete(id);
|
|
1821
1821
|
reject(this.withStderr(`Timed out waiting for ${type} response`));
|
|
1822
1822
|
}, this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
|
|
1823
|
-
this.pending.set(id, { command: type, resolve
|
|
1823
|
+
this.pending.set(id, { command: type, resolve, reject, timer });
|
|
1824
1824
|
this.write({ ...fields, id, type }, (error) => {
|
|
1825
1825
|
if (!error)
|
|
1826
1826
|
return;
|
|
@@ -2036,12 +2036,12 @@ class RpcProcess {
|
|
|
2036
2036
|
}
|
|
2037
2037
|
waitForAgentSettled() {
|
|
2038
2038
|
let waiter;
|
|
2039
|
-
const promise = new Promise((
|
|
2039
|
+
const promise = new Promise((resolve, reject) => {
|
|
2040
2040
|
const timer = setTimeout(() => {
|
|
2041
2041
|
this.settledWaiters.delete(waiter);
|
|
2042
2042
|
reject(this.withStderr("Timed out waiting for agent_settled"));
|
|
2043
2043
|
}, this.options.settleTimeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS);
|
|
2044
|
-
waiter = { resolve
|
|
2044
|
+
waiter = { resolve, reject, timer };
|
|
2045
2045
|
this.settledWaiters.add(waiter);
|
|
2046
2046
|
});
|
|
2047
2047
|
return {
|
|
@@ -2060,13 +2060,13 @@ class RpcProcess {
|
|
|
2060
2060
|
}
|
|
2061
2061
|
this.settledWaiters.clear();
|
|
2062
2062
|
}
|
|
2063
|
-
write(
|
|
2063
|
+
write(record, callback) {
|
|
2064
2064
|
const child = this.child;
|
|
2065
2065
|
if (!child || child.stdin.destroyed) {
|
|
2066
2066
|
callback(this.notRunningError());
|
|
2067
2067
|
return;
|
|
2068
2068
|
}
|
|
2069
|
-
child.stdin.write(`${JSON.stringify(
|
|
2069
|
+
child.stdin.write(`${JSON.stringify(record)}
|
|
2070
2070
|
`, callback);
|
|
2071
2071
|
}
|
|
2072
2072
|
handleChildError(child, error, prefix) {
|
|
@@ -2196,18 +2196,18 @@ async function resolveWorkerAuth(registry, provider, signal, model) {
|
|
|
2196
2196
|
timer = setTimeout(() => reject(new Error("Subagent provider authentication lookup timed out after 30 seconds")), 30000);
|
|
2197
2197
|
});
|
|
2198
2198
|
try {
|
|
2199
|
-
const
|
|
2199
|
+
const resolve = async () => {
|
|
2200
2200
|
try {
|
|
2201
2201
|
const providerAuth = await registry.getProviderAuth(provider);
|
|
2202
2202
|
if (signal.aborted || !providerAuth)
|
|
2203
2203
|
return;
|
|
2204
2204
|
if (model && typeof registry.getApiKeyAndHeaders === "function") {
|
|
2205
|
-
const
|
|
2206
|
-
if (!
|
|
2205
|
+
const result = await registry.getApiKeyAndHeaders(model);
|
|
2206
|
+
if (!result.ok)
|
|
2207
2207
|
throw new Error;
|
|
2208
2208
|
return {
|
|
2209
|
-
auth: { apiKey:
|
|
2210
|
-
env:
|
|
2209
|
+
auth: { apiKey: result.apiKey, headers: result.headers, baseUrl: result.baseUrl },
|
|
2210
|
+
env: result.env
|
|
2211
2211
|
};
|
|
2212
2212
|
}
|
|
2213
2213
|
return { auth: providerAuth.auth, env: providerAuth.env };
|
|
@@ -2215,7 +2215,7 @@ async function resolveWorkerAuth(registry, provider, signal, model) {
|
|
|
2215
2215
|
throw new Error("Could not resolve subagent provider authentication");
|
|
2216
2216
|
}
|
|
2217
2217
|
};
|
|
2218
|
-
const result = await Promise.race([
|
|
2218
|
+
const result = await Promise.race([resolve(), interrupted]);
|
|
2219
2219
|
if (signal.aborted)
|
|
2220
2220
|
throw new Error("Task cancelled");
|
|
2221
2221
|
try {
|
|
@@ -2807,7 +2807,8 @@ function taskProgress(task) {
|
|
|
2807
2807
|
return progress;
|
|
2808
2808
|
}
|
|
2809
2809
|
function workerDialogTitle(task, title) {
|
|
2810
|
-
|
|
2810
|
+
const label = `@${task.agent} · ${task.description} · ${task.id.slice(0, 8)}`;
|
|
2811
|
+
return title === undefined ? label : `${label}
|
|
2811
2812
|
${title}`;
|
|
2812
2813
|
}
|
|
2813
2814
|
function messageContentText(message) {
|
|
@@ -2873,14 +2874,14 @@ var OAUTH_REFRESH_HOSTS = {
|
|
|
2873
2874
|
};
|
|
2874
2875
|
function modelEndpoint(ctx, selectedModel) {
|
|
2875
2876
|
const models = ctx.modelRegistry?.getAll() ?? [];
|
|
2876
|
-
const qualified = models.find((
|
|
2877
|
-
const byId = models.filter((
|
|
2877
|
+
const qualified = models.find((model) => `${model.provider}/${model.id}` === selectedModel);
|
|
2878
|
+
const byId = models.filter((model) => model.id === selectedModel);
|
|
2878
2879
|
const registered = qualified ?? (byId.length === 1 ? byId[0] : undefined);
|
|
2879
2880
|
const activeName = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
2880
2881
|
const active = selectedModel === activeName || byId.length === 0 && selectedModel === ctx.model?.id ? ctx.model : undefined;
|
|
2881
2882
|
const model = registered ?? active;
|
|
2882
2883
|
if (!model)
|
|
2883
|
-
return {
|
|
2884
|
+
return { error: byId.length > 1 ? "Ambiguous model" : "Model not found" };
|
|
2884
2885
|
const domains = [];
|
|
2885
2886
|
const endpoint = {
|
|
2886
2887
|
provider: model.provider,
|
|
@@ -2897,20 +2898,21 @@ function modelEndpoint(ctx, selectedModel) {
|
|
|
2897
2898
|
domains.push(refreshHost);
|
|
2898
2899
|
return endpoint;
|
|
2899
2900
|
}
|
|
2900
|
-
async function workerEndpoint(ctx, selectedModel, signal) {
|
|
2901
|
+
async function workerEndpoint(ctx, selectedModel, agentName, signal) {
|
|
2901
2902
|
if (signal.aborted)
|
|
2902
2903
|
throw new Error("Task cancelled");
|
|
2903
2904
|
const endpoint = modelEndpoint(ctx, selectedModel);
|
|
2905
|
+
if ("error" in endpoint) {
|
|
2906
|
+
throw new Error(`${endpoint.error} for subagent ${agentName}: ${selectedModel}`);
|
|
2907
|
+
}
|
|
2904
2908
|
const { provider, domains, selected } = endpoint;
|
|
2905
|
-
if (provider === undefined)
|
|
2906
|
-
return endpoint;
|
|
2907
2909
|
const auth = await resolveWorkerAuth(ctx.modelRegistry, provider, signal, selected);
|
|
2908
2910
|
const host = workerAuthHost(auth?.auth.baseUrl);
|
|
2909
2911
|
const granted = [...new Set(host ? [...domains, host] : domains)];
|
|
2910
2912
|
return {
|
|
2911
2913
|
...endpoint,
|
|
2912
2914
|
domains: granted,
|
|
2913
|
-
resolveAuth: auth
|
|
2915
|
+
resolveAuth: auth ? workerAuthResolver(ctx.modelRegistry, selected, granted) : undefined
|
|
2914
2916
|
};
|
|
2915
2917
|
}
|
|
2916
2918
|
function agentBootstrapPaths(agentDir, privateAuth) {
|
|
@@ -2964,7 +2966,7 @@ class Semaphore {
|
|
|
2964
2966
|
this.active += 1;
|
|
2965
2967
|
return this.releaseOnce();
|
|
2966
2968
|
}
|
|
2967
|
-
await new Promise((
|
|
2969
|
+
await new Promise((resolve, reject) => {
|
|
2968
2970
|
const abort = () => {
|
|
2969
2971
|
const index = this.waiters.indexOf(start);
|
|
2970
2972
|
if (index >= 0)
|
|
@@ -2974,7 +2976,7 @@ class Semaphore {
|
|
|
2974
2976
|
const start = () => {
|
|
2975
2977
|
signal?.removeEventListener("abort", abort);
|
|
2976
2978
|
this.active += 1;
|
|
2977
|
-
|
|
2979
|
+
resolve();
|
|
2978
2980
|
};
|
|
2979
2981
|
this.waiters.push(start);
|
|
2980
2982
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -3638,7 +3640,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
3638
3640
|
selectedTask = Math.max(0, Math.min(nextSelected < 0 ? 0 : nextSelected, tasks.length - 1));
|
|
3639
3641
|
};
|
|
3640
3642
|
const taskTargets = () => {
|
|
3641
|
-
const selected = tasks.filter((
|
|
3643
|
+
const selected = tasks.filter((task) => selectedTaskIds.has(task.id));
|
|
3642
3644
|
if (selected.length > 0)
|
|
3643
3645
|
return selected;
|
|
3644
3646
|
const task = tasks[selectedTask];
|
|
@@ -3770,22 +3772,22 @@ ${this.primaryAgent.prompt}` };
|
|
|
3770
3772
|
const listedAgents = agentsForTab();
|
|
3771
3773
|
const agentRows = listRowBudget(tui.terminal?.rows, AGENT_LIST_ROWS, AGENT_TAB_CHROME_ROWS);
|
|
3772
3774
|
const { shown, start, status } = listWindow(listedAgents, selectedAgent, agentRows);
|
|
3773
|
-
const nameWidth = Math.min(20, Math.max(5, ...listedAgents.map((
|
|
3775
|
+
const nameWidth = Math.min(20, Math.max(5, ...listedAgents.map((agent) => visibleWidth(`@${agent.name}`))));
|
|
3774
3776
|
const modelWidth = Math.max(10, Math.min(24, Math.floor((contentWidth - 2) / 3)));
|
|
3775
3777
|
const enabledWidth = 13;
|
|
3776
3778
|
const runtimeWidth = Math.max(12, contentWidth - 2 - nameWidth - 1 - modelWidth - 1 - enabledWidth - 1);
|
|
3777
|
-
const runtimeFor = (
|
|
3779
|
+
const runtimeFor = (agent) => {
|
|
3778
3780
|
const parts = [];
|
|
3779
|
-
if (
|
|
3781
|
+
if (agent.name === this.primaryAgent?.name)
|
|
3780
3782
|
parts.push("★ primary");
|
|
3781
|
-
const byState = (state) => [...this.tasks.values()].filter((task) => task.agent ===
|
|
3783
|
+
const byState = (state) => [...this.tasks.values()].filter((task) => task.agent === agent.name && task.state === state).length;
|
|
3782
3784
|
const running = byState("running");
|
|
3783
3785
|
const queued = byState("queued");
|
|
3784
3786
|
if (running > 0)
|
|
3785
3787
|
parts.push(`${running} running`);
|
|
3786
3788
|
if (queued > 0)
|
|
3787
3789
|
parts.push(`${queued} queued`);
|
|
3788
|
-
const rules = [...catalog.permissions, ...
|
|
3790
|
+
const rules = [...catalog.permissions, ...agent.permissions].length;
|
|
3789
3791
|
parts.push(rules > 0 ? `${rules} rules` : "ask by default");
|
|
3790
3792
|
return parts.length > 0 ? parts.join(" · ") : "—";
|
|
3791
3793
|
};
|
|
@@ -3794,20 +3796,20 @@ ${this.primaryAgent.prompt}` };
|
|
|
3794
3796
|
lines.push(`${theme.fg("accent", "Filter")} ${agentFilter}${filteringAgents ? theme.fg("accent", "█") : ""}`);
|
|
3795
3797
|
}
|
|
3796
3798
|
lines.push(theme.fg("dim", ` ${pad("Agent", nameWidth)} ${pad("Model", modelWidth)} ${pad("Enabled", enabledWidth)} ${pad("Runtime", runtimeWidth)}`));
|
|
3797
|
-
for (const [offset,
|
|
3799
|
+
for (const [offset, agent] of shown.entries()) {
|
|
3798
3800
|
const index = start + offset;
|
|
3799
3801
|
const selected = index === selectedAgent;
|
|
3800
3802
|
const cursor = selected ? theme.fg("accent", "›") : " ";
|
|
3801
|
-
const disabled =
|
|
3802
|
-
const deleting = confirmingDeleteAgent ===
|
|
3803
|
-
const inherited = !disabledOverrides.project.has(
|
|
3803
|
+
const disabled = agent.disabled;
|
|
3804
|
+
const deleting = confirmingDeleteAgent === agent.name;
|
|
3805
|
+
const inherited = !disabledOverrides.project.has(agent.name);
|
|
3804
3806
|
const enabled = inherited ? `inherited ${disabled ? "off" : "on"}` : disabled ? "disabled" : "enabled";
|
|
3805
3807
|
const color = deleting ? "error" : disabled ? "muted" : "text";
|
|
3806
|
-
const plainName = pad(`@${
|
|
3807
|
-
const name = deleting ? theme.fg("error", plainName) : disabled ? theme.fg("muted", plainName) : colorizeAgentText(
|
|
3808
|
-
const runtime = runtimeFor(
|
|
3809
|
-
const line = `${cursor} ${name} ${theme.fg(color, pad(
|
|
3810
|
-
lines.push(
|
|
3808
|
+
const plainName = pad(`@${agent.name}`, nameWidth);
|
|
3809
|
+
const name = deleting ? theme.fg("error", plainName) : disabled ? theme.fg("muted", plainName) : colorizeAgentText(agent.color, plainName, (agentColor, text) => theme.fg(agentColor, text));
|
|
3810
|
+
const runtime = runtimeFor(agent);
|
|
3811
|
+
const line = `${cursor} ${name} ${theme.fg(color, pad(agent.model ?? "current model", modelWidth))} ${theme.fg(deleting ? "error" : disabled ? "warning" : color, pad(enabled, enabledWidth))} ${theme.fg(disabled ? "muted" : "dim", pad(runtime, runtimeWidth))}`;
|
|
3812
|
+
lines.push(agent.name === this.primaryAgent?.name ? theme.bold(line) : line);
|
|
3811
3813
|
}
|
|
3812
3814
|
if (listedAgents.length === 0) {
|
|
3813
3815
|
lines.push(theme.fg("muted", agentFilter ? ` No ${tab} agents match ${agentFilter}` : ` No ${tab} agents configured.`));
|
|
@@ -4584,22 +4586,22 @@ ${catalog.diagnostics.join(`
|
|
|
4584
4586
|
if (input.task_id)
|
|
4585
4587
|
task.delivered = false;
|
|
4586
4588
|
if (existingRun) {
|
|
4587
|
-
const
|
|
4589
|
+
const controller = this.controllers.get(task.id);
|
|
4588
4590
|
let rejectCancelled;
|
|
4589
4591
|
const cancelled = new Promise((_resolve, reject) => {
|
|
4590
4592
|
rejectCancelled = reject;
|
|
4591
4593
|
});
|
|
4592
|
-
const
|
|
4593
|
-
|
|
4594
|
+
const abort = () => {
|
|
4595
|
+
controller?.abort();
|
|
4594
4596
|
rejectCancelled?.(new Error("Task cancelled"));
|
|
4595
4597
|
};
|
|
4596
4598
|
const waitFor = (promise) => signal && !input.background ? Promise.race([promise, cancelled]) : promise;
|
|
4597
4599
|
if (!input.background && signal?.aborted) {
|
|
4598
|
-
|
|
4600
|
+
controller?.abort();
|
|
4599
4601
|
throw new Error("Task cancelled");
|
|
4600
4602
|
}
|
|
4601
4603
|
if (!input.background)
|
|
4602
|
-
signal?.addEventListener("abort",
|
|
4604
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
4603
4605
|
try {
|
|
4604
4606
|
const running = this.running.get(task.id);
|
|
4605
4607
|
if (running) {
|
|
@@ -4629,7 +4631,7 @@ ${catalog.diagnostics.join(`
|
|
|
4629
4631
|
this.releaseForeground(task.id);
|
|
4630
4632
|
}
|
|
4631
4633
|
} finally {
|
|
4632
|
-
signal?.removeEventListener("abort",
|
|
4634
|
+
signal?.removeEventListener("abort", abort);
|
|
4633
4635
|
}
|
|
4634
4636
|
}
|
|
4635
4637
|
task.state = "queued";
|
|
@@ -4880,7 +4882,7 @@ ${catalog.diagnostics.join(`
|
|
|
4880
4882
|
const model = agent.model ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
4881
4883
|
if (!model)
|
|
4882
4884
|
throw new Error(`No model available for subagent ${agent.name}`);
|
|
4883
|
-
const endpoint = await workerEndpoint(ctx, model, signal);
|
|
4885
|
+
const endpoint = await workerEndpoint(ctx, model, agent.name, signal);
|
|
4884
4886
|
if (signal.aborted)
|
|
4885
4887
|
throw new Error("Task cancelled");
|
|
4886
4888
|
const thinking = agent.variant && PI_THINKING_LEVELS.has(agent.variant) ? agent.variant : this.pi.getThinkingLevel();
|
|
@@ -4905,7 +4907,8 @@ ${catalog.diagnostics.join(`
|
|
|
4905
4907
|
join7(packageDir, "index.ts"),
|
|
4906
4908
|
...workerExtensions.flatMap((extension) => ["--extension", extension.entry]),
|
|
4907
4909
|
...task.sessionFile ? ["--session", task.sessionFile] : task.sessionDir ? ["--session-dir", task.sessionDir] : [],
|
|
4908
|
-
|
|
4910
|
+
"--provider",
|
|
4911
|
+
endpoint.provider,
|
|
4909
4912
|
"--model",
|
|
4910
4913
|
endpoint.model,
|
|
4911
4914
|
"--thinking",
|
|
@@ -4934,6 +4937,7 @@ ${catalog.diagnostics.join(`
|
|
|
4934
4937
|
const modelsStorePath = join7(agentDir, "models-store.json");
|
|
4935
4938
|
const cliEntry = invocation.args[0] ?? invocation.command;
|
|
4936
4939
|
launch = await this.integration.prepareRpcWorker({
|
|
4940
|
+
permissionPromptLabel: workerDialogTitle(task),
|
|
4937
4941
|
command: invocation.command,
|
|
4938
4942
|
args,
|
|
4939
4943
|
cwd: ctx.cwd,
|
|
@@ -5148,7 +5152,7 @@ ${catalog.diagnostics.join(`
|
|
|
5148
5152
|
}
|
|
5149
5153
|
async forwardWorkerUi(ctx, task, request, signal) {
|
|
5150
5154
|
if (request.method === "select" && typeof request.title === "string" && Array.isArray(request.options)) {
|
|
5151
|
-
const options = request.options.filter((
|
|
5155
|
+
const options = request.options.filter((value) => typeof value === "string");
|
|
5152
5156
|
const value = await ctx.ui.select(workerDialogTitle(task, request.title), options, {
|
|
5153
5157
|
signal
|
|
5154
5158
|
});
|
|
@@ -5257,10 +5261,10 @@ ${renderTaskResult(task.id, "error", message)}`);
|
|
|
5257
5261
|
const tree = taskTreeLines(visible, (task) => {
|
|
5258
5262
|
const state = taskState(theme, task);
|
|
5259
5263
|
const agent = theme.fg("accent", `@${task.agent}`);
|
|
5260
|
-
const
|
|
5261
|
-
const progress = taskProgress(
|
|
5264
|
+
const record = byId.get(task.id);
|
|
5265
|
+
const progress = taskProgress(record);
|
|
5262
5266
|
const suffix = progress.length > 0 ? theme.fg("dim", ` · ${progress.join(" · ")}`) : "";
|
|
5263
|
-
const indentWidth = 3 + Math.max(0,
|
|
5267
|
+
const indentWidth = 3 + Math.max(0, record.depth) * 3;
|
|
5264
5268
|
const descriptionWidth = Math.max(1, width - indentWidth - visibleWidth(`${state} ${agent} ${suffix}`));
|
|
5265
5269
|
const description = theme.fg("text", truncateToWidth(task.description, descriptionWidth));
|
|
5266
5270
|
return `${state} ${agent} ${description}${suffix}`;
|
|
@@ -5415,8 +5419,8 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
|
|
|
5415
5419
|
}
|
|
5416
5420
|
if (agent.model) {
|
|
5417
5421
|
const models = ctx.modelRegistry.getAll();
|
|
5418
|
-
const qualified = models.filter((
|
|
5419
|
-
const matches = qualified.length > 0 ? qualified : models.filter((
|
|
5422
|
+
const qualified = models.filter((model) => `${model.provider}/${model.id}` === agent.model);
|
|
5423
|
+
const matches = qualified.length > 0 ? qualified : models.filter((model) => model.id === agent.model);
|
|
5420
5424
|
if (matches.length === 0) {
|
|
5421
5425
|
ctx.ui.notify(`Model not found for primary agent ${agent.name}: ${agent.model}`, "error");
|
|
5422
5426
|
return false;
|
|
@@ -5815,8 +5819,8 @@ function matchesPattern(filePath, patterns, cwd, explicitDenyGlobRoots) {
|
|
|
5815
5819
|
}
|
|
5816
5820
|
return roots.some((target) => abs === target || abs.startsWith(target.endsWith("/") ? target : `${target}/`));
|
|
5817
5821
|
}
|
|
5818
|
-
const
|
|
5819
|
-
return abs === absPattern || abs.startsWith(absPattern +
|
|
5822
|
+
const sep = absPattern.endsWith("/") ? "" : "/";
|
|
5823
|
+
return abs === absPattern || abs.startsWith(absPattern + sep);
|
|
5820
5824
|
});
|
|
5821
5825
|
}
|
|
5822
5826
|
function normalizeBlockedPath(path, cwd) {
|
|
@@ -5867,9 +5871,32 @@ function extractNativeDeniedPath(output, cwd) {
|
|
|
5867
5871
|
return normalizePathMatch(match[1], cwd);
|
|
5868
5872
|
return null;
|
|
5869
5873
|
}
|
|
5874
|
+
function extractDeniedPathMention(output, cwd, denied) {
|
|
5875
|
+
const start = /(?<=^|['"`\s=:(])(?=\/|[a-zA-Z]:[\\/])/g;
|
|
5876
|
+
for (const line of output.split(/\r?\n/)) {
|
|
5877
|
+
for (const match of line.matchAll(start)) {
|
|
5878
|
+
const raw = line.slice(match.index);
|
|
5879
|
+
const quoted = raw.match(/^([^'"`]+)['"`]/);
|
|
5880
|
+
const words = (quoted ? quoted[1] : raw).replace(/[.,:;)\]]+$/, "").split(" ");
|
|
5881
|
+
for (let count = words.length;count > 0; count -= 1) {
|
|
5882
|
+
const candidate = words.slice(0, count).join(" ");
|
|
5883
|
+
if (!existsSync6(candidate))
|
|
5884
|
+
continue;
|
|
5885
|
+
const path = normalizeBlockedPath(candidate, cwd);
|
|
5886
|
+
if (denied(path))
|
|
5887
|
+
return path;
|
|
5888
|
+
break;
|
|
5889
|
+
}
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
return null;
|
|
5893
|
+
}
|
|
5870
5894
|
function extractRetryableNativeReadDeniedPath(output, cwd, allowRead, denyRead) {
|
|
5895
|
+
const denied = (path) => !readAllowed(path, allowRead, denyRead, cwd);
|
|
5871
5896
|
const path = extractNativeDeniedPath(output, cwd);
|
|
5872
|
-
|
|
5897
|
+
if (path)
|
|
5898
|
+
return denied(path) ? path : null;
|
|
5899
|
+
return extractDeniedPathMention(output, cwd, denied);
|
|
5873
5900
|
}
|
|
5874
5901
|
function extractNativeWriteDeniedPath(output, cwd) {
|
|
5875
5902
|
const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
|
|
@@ -5935,23 +5962,24 @@ async function stopPreparedChild(child) {
|
|
|
5935
5962
|
child.kill("SIGKILL");
|
|
5936
5963
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
5937
5964
|
return;
|
|
5938
|
-
await new Promise((
|
|
5965
|
+
await new Promise((resolve) => {
|
|
5939
5966
|
let settleTimer;
|
|
5940
5967
|
const done = () => {
|
|
5941
5968
|
if (settleTimer)
|
|
5942
5969
|
clearTimeout(settleTimer);
|
|
5943
5970
|
child.removeListener("exit", done);
|
|
5944
|
-
|
|
5971
|
+
resolve();
|
|
5945
5972
|
};
|
|
5946
5973
|
child.once("exit", done);
|
|
5947
5974
|
settleTimer = setTimeout(done, 1000);
|
|
5948
5975
|
});
|
|
5949
5976
|
}
|
|
5950
|
-
async function showPermissionPrompt(ctx, title, options, signal) {
|
|
5977
|
+
async function showPermissionPrompt(ctx, title, options, signal, label) {
|
|
5951
5978
|
if (!ctx.hasUI)
|
|
5952
5979
|
return "abort";
|
|
5953
|
-
const labels = options.map((
|
|
5954
|
-
const selected = await ctx.ui.select(
|
|
5980
|
+
const labels = options.map((option) => option.hint ? `${option.label} — ${option.hint}` : option.label);
|
|
5981
|
+
const selected = await ctx.ui.select(label ? `${label}
|
|
5982
|
+
${title}` : title, labels, { signal });
|
|
5955
5983
|
if (signal?.aborted)
|
|
5956
5984
|
return "abort";
|
|
5957
5985
|
const index = selected === undefined ? -1 : labels.indexOf(selected);
|
|
@@ -5959,7 +5987,8 @@ async function showPermissionPrompt(ctx, title, options, signal) {
|
|
|
5959
5987
|
if (!option)
|
|
5960
5988
|
return "abort";
|
|
5961
5989
|
if (option.confirm) {
|
|
5962
|
-
const confirmed = await ctx.ui.confirm(
|
|
5990
|
+
const confirmed = await ctx.ui.confirm(label ? `${label}
|
|
5991
|
+
Save this permission?` : "Save this permission?", `${title}
|
|
5963
5992
|
|
|
5964
5993
|
${option.hint ?? "This changes persisted sandbox policy."}`, { signal });
|
|
5965
5994
|
if (!confirmed || signal?.aborted)
|
|
@@ -5967,9 +5996,9 @@ ${option.hint ?? "This changes persisted sandbox policy."}`, { signal });
|
|
|
5967
5996
|
}
|
|
5968
5997
|
return option.action;
|
|
5969
5998
|
}
|
|
5970
|
-
function promptReadBlock(ctx, filePath, reason, signal) {
|
|
5999
|
+
function promptReadBlock(ctx, filePath, reason, signal, label) {
|
|
5971
6000
|
const title = reason ? `Read blocked: "${filePath}" is in denyRead (${reason})` : `Read blocked: "${filePath}" is not in allowRead`;
|
|
5972
|
-
return showPermissionPrompt(ctx, title, PERMISSION_OPTIONS, signal);
|
|
6001
|
+
return showPermissionPrompt(ctx, title, PERMISSION_OPTIONS, signal, label);
|
|
5973
6002
|
}
|
|
5974
6003
|
function writeEnvFile(env, proxyPort, proxyToken) {
|
|
5975
6004
|
const lines = [];
|
|
@@ -6285,7 +6314,7 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
|
|
|
6285
6314
|
target.push(scope);
|
|
6286
6315
|
noteScope(ctx, "Write", choice, filePath, scope);
|
|
6287
6316
|
}
|
|
6288
|
-
async function authorizeFilesystemToolAccess(ctx, accesses,
|
|
6317
|
+
async function authorizeFilesystemToolAccess(ctx, accesses, options = {}) {
|
|
6289
6318
|
const normalized = [
|
|
6290
6319
|
...new Map(accesses.map((access) => {
|
|
6291
6320
|
const path = normalizeBlockedPath(access.path, ctx.cwd);
|
|
@@ -6317,9 +6346,9 @@ function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
|
|
|
6317
6346
|
const blocked = normalized.filter((access) => access.operation === "read" ? !readAllowed(access.path, getEffectiveAllowRead(config, ctx.cwd), getEffectiveDenyRead(config, ctx.cwd), ctx.cwd) : shouldPromptForWrite(access.path, getEffectiveAllowWrite(config), ctx.cwd));
|
|
6318
6347
|
const paths = blocked.map(({ operation, path }) => `${operation}: ${path}`).join(`
|
|
6319
6348
|
`);
|
|
6320
|
-
const note =
|
|
6349
|
+
const note = options.authorizationNote ? `
|
|
6321
6350
|
|
|
6322
|
-
${
|
|
6351
|
+
${options.authorizationNote}` : "";
|
|
6323
6352
|
const choice = await showPermissionPrompt(ctx, `Filesystem policy blocked this tool call:
|
|
6324
6353
|
${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
6325
6354
|
if (choice === "abort" || promptSignal.aborted) {
|
|
@@ -6333,7 +6362,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6333
6362
|
}
|
|
6334
6363
|
}
|
|
6335
6364
|
return { allowed: true, prompted: true };
|
|
6336
|
-
},
|
|
6365
|
+
}, options.signal);
|
|
6337
6366
|
}
|
|
6338
6367
|
async function ensureDomainAllowed(ctx, domain, cwd, allowances, signal) {
|
|
6339
6368
|
const current = () => {
|
|
@@ -6348,7 +6377,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6348
6377
|
if (!ctx.hasUI)
|
|
6349
6378
|
return current() ?? false;
|
|
6350
6379
|
return permissionPrompts.resolve(current, async (promptSignal) => {
|
|
6351
|
-
const choice = await showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in the allowed domains list`, PERMISSION_OPTIONS, promptSignal);
|
|
6380
|
+
const choice = await showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in the allowed domains list`, PERMISSION_OPTIONS, promptSignal, allowances?.permissionPromptLabel);
|
|
6352
6381
|
if (choice === "abort" || promptSignal.aborted)
|
|
6353
6382
|
return false;
|
|
6354
6383
|
await applyDomainChoice(choice, domain, cwd, allowances);
|
|
@@ -6398,7 +6427,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6398
6427
|
});
|
|
6399
6428
|
}
|
|
6400
6429
|
function createSocketPair() {
|
|
6401
|
-
return new Promise((
|
|
6430
|
+
return new Promise((resolve, reject) => {
|
|
6402
6431
|
const server = createServer();
|
|
6403
6432
|
server.on("error", reject);
|
|
6404
6433
|
let client = null;
|
|
@@ -6407,7 +6436,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6407
6436
|
server.close();
|
|
6408
6437
|
if (client) {
|
|
6409
6438
|
client.removeListener("error", reject);
|
|
6410
|
-
|
|
6439
|
+
resolve([client, serverEnd]);
|
|
6411
6440
|
}
|
|
6412
6441
|
});
|
|
6413
6442
|
server.listen(0, "127.0.0.1", () => {
|
|
@@ -6421,14 +6450,14 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6421
6450
|
async function resolveTrapQuery(trap, ctx, cwd, allowances, promptOnBlock, signal) {
|
|
6422
6451
|
if (!isFilesystemTrap(trap)) {
|
|
6423
6452
|
const key = `${trap.operation}\x00${trap.target}`;
|
|
6424
|
-
const
|
|
6425
|
-
const
|
|
6426
|
-
if (
|
|
6427
|
-
return
|
|
6453
|
+
const current = () => sessionAllowedTargets.includes(key) || allowances.targets.includes(key) ? { action: "allow" } : undefined;
|
|
6454
|
+
const existing = current();
|
|
6455
|
+
if (existing)
|
|
6456
|
+
return existing;
|
|
6428
6457
|
if (!ctx.hasUI || !promptOnBlock)
|
|
6429
6458
|
return { action: "deny", reason: "unprompted" };
|
|
6430
|
-
return permissionPrompts.resolve(
|
|
6431
|
-
const choice = await showPermissionPrompt(ctx, `Network blocked: ${trap.operation} "${trap.target}"`, NETWORK_PERMISSION_OPTIONS, promptSignal);
|
|
6459
|
+
return permissionPrompts.resolve(current, async (promptSignal) => {
|
|
6460
|
+
const choice = await showPermissionPrompt(ctx, `Network blocked: ${trap.operation} "${trap.target}"`, NETWORK_PERMISSION_OPTIONS, promptSignal, allowances.permissionPromptLabel);
|
|
6432
6461
|
if (choice === "abort" || promptSignal.aborted)
|
|
6433
6462
|
return { action: "deny", reason: "rejected" };
|
|
6434
6463
|
const targets = choice === "once" ? allowances.targets : sessionAllowedTargets;
|
|
@@ -6462,7 +6491,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6462
6491
|
return { action: "deny", reason: "unprompted" };
|
|
6463
6492
|
return permissionPrompts.resolve(current, async (promptSignal) => {
|
|
6464
6493
|
const config = loadConfig(cwd);
|
|
6465
|
-
const choice = trap.operation === "read" ? await promptReadBlock(ctx, path, matchesPattern(path, config.filesystem.denyRead, cwd) ? "granting allowRead will override it" : undefined, promptSignal) : await showPermissionPrompt(ctx, `Write blocked: "${path}" is not in allowWrite`, PERMISSION_OPTIONS, promptSignal);
|
|
6494
|
+
const choice = trap.operation === "read" ? await promptReadBlock(ctx, path, matchesPattern(path, config.filesystem.denyRead, cwd) ? "granting allowRead will override it" : undefined, promptSignal, allowances.permissionPromptLabel) : await showPermissionPrompt(ctx, `Write blocked: "${path}" is not in allowWrite`, PERMISSION_OPTIONS, promptSignal, allowances.permissionPromptLabel);
|
|
6466
6495
|
if (choice === "abort" || promptSignal.aborted)
|
|
6467
6496
|
return { action: "deny", reason: "rejected" };
|
|
6468
6497
|
if (trap.operation === "read") {
|
|
@@ -6519,7 +6548,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6519
6548
|
}
|
|
6520
6549
|
});
|
|
6521
6550
|
}
|
|
6522
|
-
function prepareUnsandboxedRpcWorker(
|
|
6551
|
+
function prepareUnsandboxedRpcWorker(options) {
|
|
6523
6552
|
let spawned = false;
|
|
6524
6553
|
let disposed = false;
|
|
6525
6554
|
let disposePromise;
|
|
@@ -6532,8 +6561,8 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6532
6561
|
spawned = true;
|
|
6533
6562
|
const child = spawn2(command, [...args], {
|
|
6534
6563
|
...spawnOptions,
|
|
6535
|
-
cwd:
|
|
6536
|
-
env:
|
|
6564
|
+
cwd: options.cwd,
|
|
6565
|
+
env: options.env,
|
|
6537
6566
|
detached: true
|
|
6538
6567
|
});
|
|
6539
6568
|
patchProcessGroupKill(child);
|
|
@@ -6541,10 +6570,10 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6541
6570
|
return child;
|
|
6542
6571
|
};
|
|
6543
6572
|
return {
|
|
6544
|
-
command:
|
|
6545
|
-
args:
|
|
6546
|
-
cwd:
|
|
6547
|
-
env:
|
|
6573
|
+
command: options.command,
|
|
6574
|
+
args: options.args,
|
|
6575
|
+
cwd: options.cwd,
|
|
6576
|
+
env: options.env,
|
|
6548
6577
|
spawn: spawnWorker,
|
|
6549
6578
|
dispose() {
|
|
6550
6579
|
disposePromise ??= (async () => {
|
|
@@ -6555,65 +6584,66 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6555
6584
|
}
|
|
6556
6585
|
};
|
|
6557
6586
|
}
|
|
6558
|
-
async function prepareRpcWorker(
|
|
6559
|
-
if (
|
|
6587
|
+
async function prepareRpcWorker(options) {
|
|
6588
|
+
if (options.signal?.aborted)
|
|
6560
6589
|
throw new Error("Task cancelled");
|
|
6561
|
-
if (!ensureSandboxState(
|
|
6590
|
+
if (!ensureSandboxState(options.ctx)) {
|
|
6562
6591
|
if (sandboxState === "unavailable") {
|
|
6563
6592
|
throw new Error("Sandbox is unavailable; refusing subagent process");
|
|
6564
6593
|
}
|
|
6565
6594
|
if (!unsandboxedWorkerWarningShown) {
|
|
6566
|
-
warnUnsandboxed(
|
|
6595
|
+
warnUnsandboxed(options.ctx, "Subagent processes are running without Landstrip sandboxing", "warning");
|
|
6567
6596
|
unsandboxedWorkerWarningShown = true;
|
|
6568
6597
|
}
|
|
6569
|
-
return prepareUnsandboxedRpcWorker(
|
|
6598
|
+
return prepareUnsandboxedRpcWorker(options);
|
|
6570
6599
|
}
|
|
6571
6600
|
const allowances = {
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6601
|
+
permissionPromptLabel: options.permissionPromptLabel,
|
|
6602
|
+
domains: [...options.domains ?? []],
|
|
6603
|
+
readPaths: [...options.readPaths],
|
|
6604
|
+
writePaths: [...options.writePaths],
|
|
6575
6605
|
targets: [],
|
|
6576
6606
|
protectedPaths: [
|
|
6577
|
-
...new Set((
|
|
6607
|
+
...new Set((options.protectedPaths ?? []).flatMap((path) => {
|
|
6578
6608
|
if (/[*?[\]]/.test(path)) {
|
|
6579
6609
|
throw new Error(`Worker protected path "${path}" must be a concrete path, not a glob`);
|
|
6580
6610
|
}
|
|
6581
|
-
return [expandPath(path,
|
|
6611
|
+
return [expandPath(path, options.cwd), canonicalizePath2(path, options.cwd)];
|
|
6582
6612
|
}))
|
|
6583
6613
|
]
|
|
6584
6614
|
};
|
|
6585
|
-
const processAllowances = withWindowsProcessReadAccess(allowances,
|
|
6586
|
-
const config = loadConfig(
|
|
6615
|
+
const processAllowances = withWindowsProcessReadAccess(allowances, options.command, options.cwd);
|
|
6616
|
+
const config = loadConfig(options.cwd);
|
|
6587
6617
|
const explicitDenyGlobRoots = new Map;
|
|
6588
6618
|
processAllowances.explicitDenyRead = {
|
|
6589
6619
|
patterns: config.explicitDenyRead,
|
|
6590
6620
|
globRoots: explicitDenyGlobRoots
|
|
6591
6621
|
};
|
|
6592
|
-
for (const path of [
|
|
6622
|
+
for (const path of [options.cwd, options.command, ...processAllowances.readPaths]) {
|
|
6593
6623
|
if (/[*?[\]]/.test(path)) {
|
|
6594
6624
|
throw new Error(`Worker startup read path "${path}" must be a concrete path, not a glob`);
|
|
6595
6625
|
}
|
|
6596
|
-
const deny = config.explicitDenyRead.find((pattern) => matchesPattern(path, [pattern],
|
|
6626
|
+
const deny = config.explicitDenyRead.find((pattern) => matchesPattern(path, [pattern], options.cwd, explicitDenyGlobRoots));
|
|
6597
6627
|
if (deny !== undefined) {
|
|
6598
6628
|
throw new Error(`Worker startup read path "${path}" conflicts with explicit filesystem.denyRead "${deny}"; change the configured deny or the worker startup paths`);
|
|
6599
6629
|
}
|
|
6600
6630
|
}
|
|
6601
6631
|
const proxyToken = randomBytes(32).toString("base64url");
|
|
6602
6632
|
const proxyAuthorization = `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString("base64")}`;
|
|
6603
|
-
const proxy = shouldStartProxy(config) ? await startProxy(
|
|
6633
|
+
const proxy = shouldStartProxy(config) ? await startProxy(options.ctx, options.cwd, processAllowances, options.signal, proxyAuthorization, restrictedUserProxyPortRange()) : null;
|
|
6604
6634
|
let policy;
|
|
6605
6635
|
let trapSocket;
|
|
6606
6636
|
let childEnd;
|
|
6607
6637
|
let disposed = false;
|
|
6608
6638
|
try {
|
|
6609
|
-
if (
|
|
6639
|
+
if (options.signal?.aborted)
|
|
6610
6640
|
throw new Error("Task cancelled");
|
|
6611
|
-
policy = writePolicyFile(
|
|
6641
|
+
policy = writePolicyFile(options.cwd, proxy?.port ?? null, "worker", processAllowances);
|
|
6612
6642
|
if (process.platform !== "win32") {
|
|
6613
6643
|
[trapSocket, childEnd] = await createSocketPair();
|
|
6614
|
-
if (
|
|
6644
|
+
if (options.signal?.aborted)
|
|
6615
6645
|
throw new Error("Task cancelled");
|
|
6616
|
-
attachWorkerTrap(trapSocket,
|
|
6646
|
+
attachWorkerTrap(trapSocket, options.ctx, options.cwd, processAllowances, options.signal);
|
|
6617
6647
|
}
|
|
6618
6648
|
} catch (error) {
|
|
6619
6649
|
trapSocket?.destroy();
|
|
@@ -6626,7 +6656,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6626
6656
|
const workerPolicy = policy;
|
|
6627
6657
|
const workerTrapSocket = trapSocket;
|
|
6628
6658
|
const workerChildEnd = childEnd;
|
|
6629
|
-
const workerEnv = { ...
|
|
6659
|
+
const workerEnv = { ...options.env };
|
|
6630
6660
|
if (proxy) {
|
|
6631
6661
|
const url = `http://landstrip:${proxyToken}@127.0.0.1:${proxy.port}`;
|
|
6632
6662
|
for (const name of PROXY_ENVIRONMENT_VARIABLES) {
|
|
@@ -6649,8 +6679,8 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6649
6679
|
"-p",
|
|
6650
6680
|
workerPolicy.path,
|
|
6651
6681
|
"--",
|
|
6652
|
-
|
|
6653
|
-
...
|
|
6682
|
+
options.command,
|
|
6683
|
+
...options.args
|
|
6654
6684
|
];
|
|
6655
6685
|
const authPipe = Array.isArray(spawnOptions.stdio) && spawnOptions.stdio[WORKER_AUTH_FD] === "pipe";
|
|
6656
6686
|
if (authPipe && process.platform === "win32") {
|
|
@@ -6667,7 +6697,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6667
6697
|
landstripArgs.splice(1, 0, "--trap", "3");
|
|
6668
6698
|
const child = spawn2(binaryPath(), landstripArgs, {
|
|
6669
6699
|
...spawnOptions,
|
|
6670
|
-
cwd:
|
|
6700
|
+
cwd: options.cwd,
|
|
6671
6701
|
env: workerEnv,
|
|
6672
6702
|
detached: true,
|
|
6673
6703
|
stdio
|
|
@@ -6678,9 +6708,9 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6678
6708
|
return child;
|
|
6679
6709
|
};
|
|
6680
6710
|
return {
|
|
6681
|
-
command:
|
|
6682
|
-
args:
|
|
6683
|
-
cwd:
|
|
6711
|
+
command: options.command,
|
|
6712
|
+
args: options.args,
|
|
6713
|
+
cwd: options.cwd,
|
|
6684
6714
|
env: workerEnv,
|
|
6685
6715
|
spawn: spawnWorker,
|
|
6686
6716
|
dispose() {
|
|
@@ -6696,15 +6726,15 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
6696
6726
|
}
|
|
6697
6727
|
};
|
|
6698
6728
|
}
|
|
6699
|
-
async function prepareProcess(
|
|
6700
|
-
if (process.platform === "win32" && (getNoSandboxFlag() || !loadConfig(
|
|
6729
|
+
async function prepareProcess(options) {
|
|
6730
|
+
if (process.platform === "win32" && (getNoSandboxFlag() || !loadConfig(options.cwd).enabled)) {
|
|
6701
6731
|
throw new Error("Generic process preparation requires sandboxing on Windows");
|
|
6702
6732
|
}
|
|
6703
6733
|
return prepareRpcWorker({
|
|
6704
|
-
...
|
|
6705
|
-
env:
|
|
6706
|
-
readPaths:
|
|
6707
|
-
writePaths:
|
|
6734
|
+
...options,
|
|
6735
|
+
env: options.env ?? process.env,
|
|
6736
|
+
readPaths: options.readPaths ?? [],
|
|
6737
|
+
writePaths: options.writePaths ?? []
|
|
6708
6738
|
});
|
|
6709
6739
|
}
|
|
6710
6740
|
function createProviderBashOps() {
|
|
@@ -7096,16 +7126,16 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
7096
7126
|
return null;
|
|
7097
7127
|
const { globalPath, projectPath } = getPiConfigPaths(ctx.cwd, "sandbox.json");
|
|
7098
7128
|
const current = () => {
|
|
7099
|
-
const
|
|
7100
|
-
if (matchesPattern(blockedPath, operation === "read" ?
|
|
7129
|
+
const config = loadConfig(ctx.cwd);
|
|
7130
|
+
if (matchesPattern(blockedPath, operation === "read" ? config.filesystem.denyReadAlways : config.filesystem.denyWrite, ctx.cwd)) {
|
|
7101
7131
|
return false;
|
|
7102
7132
|
}
|
|
7103
|
-
const needsPrompt = operation === "read" ? !readAllowed(blockedPath, getEffectiveAllowRead(
|
|
7133
|
+
const needsPrompt = operation === "read" ? !readAllowed(blockedPath, getEffectiveAllowRead(config, ctx.cwd, allowances), getEffectiveDenyRead(config, ctx.cwd), ctx.cwd) : shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config, allowances), ctx.cwd);
|
|
7104
7134
|
return needsPrompt ? undefined : true;
|
|
7105
7135
|
};
|
|
7106
7136
|
const access = await permissionPrompts.resolve(current, async (promptSignal) => {
|
|
7107
|
-
const
|
|
7108
|
-
const choice = operation === "read" ? await promptReadBlock(ctx, blockedPath, matchesPattern(blockedPath,
|
|
7137
|
+
const config = loadConfig(ctx.cwd);
|
|
7138
|
+
const choice = operation === "read" ? await promptReadBlock(ctx, blockedPath, matchesPattern(blockedPath, config.filesystem.denyRead, ctx.cwd) ? "granting allowRead will override it" : undefined, promptSignal) : await showPermissionPrompt(ctx, `Write blocked: "${blockedPath}" is not in allowWrite`, PERMISSION_OPTIONS, promptSignal);
|
|
7109
7139
|
if (choice === "abort" || promptSignal.aborted)
|
|
7110
7140
|
return false;
|
|
7111
7141
|
if (operation === "read") {
|
|
@@ -7116,8 +7146,8 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
7116
7146
|
return true;
|
|
7117
7147
|
}, signal);
|
|
7118
7148
|
if (!access) {
|
|
7119
|
-
const
|
|
7120
|
-
if (operation === "write" && matchesPattern(blockedPath,
|
|
7149
|
+
const config = loadConfig(ctx.cwd);
|
|
7150
|
+
if (operation === "write" && matchesPattern(blockedPath, config.filesystem.denyWrite, ctx.cwd)) {
|
|
7121
7151
|
notify(ctx, `"${blockedPath}" is blocked by denyWrite. Check:
|
|
7122
7152
|
${projectPath}
|
|
7123
7153
|
${globalPath}`, "warning");
|
|
@@ -7153,21 +7183,21 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
|
|
|
7153
7183
|
const errorText = formatError(error);
|
|
7154
7184
|
const fallbackOutput = `${stderrOutput}
|
|
7155
7185
|
${errorText}`;
|
|
7156
|
-
const
|
|
7157
|
-
if (
|
|
7158
|
-
const
|
|
7159
|
-
if (
|
|
7160
|
-
return
|
|
7186
|
+
const blockedWritePath = extractTrapBlockedPath(landstripErrorOutput, ctx.cwd, "write") ?? extractNativeWriteDeniedPath(fallbackOutput, ctx.cwd);
|
|
7187
|
+
if (blockedWritePath) {
|
|
7188
|
+
const retryResult = await retryWithAccess("write", blockedWritePath);
|
|
7189
|
+
if (retryResult)
|
|
7190
|
+
return retryResult;
|
|
7161
7191
|
}
|
|
7162
|
-
const
|
|
7163
|
-
if (
|
|
7164
|
-
const
|
|
7165
|
-
if (
|
|
7166
|
-
return
|
|
7192
|
+
const blockedReadPath = blockedReadPathForRetry(fallbackOutput);
|
|
7193
|
+
if (blockedReadPath) {
|
|
7194
|
+
const retryResult = await retryWithAccess("read", blockedReadPath);
|
|
7195
|
+
if (retryResult)
|
|
7196
|
+
return retryResult;
|
|
7167
7197
|
}
|
|
7168
|
-
const
|
|
7169
|
-
if (
|
|
7170
|
-
throw new Error(formatLandstripTraps(
|
|
7198
|
+
const landstripErrors = parseLandstripTraps(landstripErrorOutput);
|
|
7199
|
+
if (landstripErrors.length > 0) {
|
|
7200
|
+
throw new Error(formatLandstripTraps(landstripErrors));
|
|
7171
7201
|
}
|
|
7172
7202
|
throw error;
|
|
7173
7203
|
}
|
|
@@ -7180,9 +7210,9 @@ ${message}
|
|
|
7180
7210
|
}
|
|
7181
7211
|
const blockedWritePath = extractTrapBlockedPath(landstripErrorOutput, ctx.cwd, "write") ?? extractNativeWriteDeniedPath(stderrOutput, ctx.cwd);
|
|
7182
7212
|
if (blockedWritePath) {
|
|
7183
|
-
const
|
|
7184
|
-
if (
|
|
7185
|
-
return
|
|
7213
|
+
const retryResult = await retryWithAccess("write", blockedWritePath);
|
|
7214
|
+
if (retryResult)
|
|
7215
|
+
return retryResult;
|
|
7186
7216
|
}
|
|
7187
7217
|
const blockedReadPath = blockedReadPathForRetry(stderrOutput);
|
|
7188
7218
|
if (!blockedReadPath)
|
|
@@ -7240,7 +7270,7 @@ ${message}
|
|
|
7240
7270
|
networkLabel = `${config.network.allowedDomains.length} domains`;
|
|
7241
7271
|
networkColor = "accent";
|
|
7242
7272
|
}
|
|
7243
|
-
const
|
|
7273
|
+
const sep = theme.fg("dim", "·");
|
|
7244
7274
|
const net = theme.fg(networkColor, networkLabel);
|
|
7245
7275
|
const write = theme.fg("accent", `${config.filesystem.allowWrite.length} write paths`);
|
|
7246
7276
|
const fields = [`${dot} ${label}`, net, write];
|
|
@@ -7249,7 +7279,7 @@ ${message}
|
|
|
7249
7279
|
const implementationLabel = windowsImplementation === "restrictedUser" ? "restricted user" : `${config.windows.appContainerMode} AppContainer`;
|
|
7250
7280
|
fields.splice(1, 0, theme.fg(standard ? "warning" : "accent", implementationLabel));
|
|
7251
7281
|
}
|
|
7252
|
-
setTuiStatus(ctx, "sandbox", fields.join(` ${
|
|
7282
|
+
setTuiStatus(ctx, "sandbox", fields.join(` ${sep} `));
|
|
7253
7283
|
}
|
|
7254
7284
|
const headlessWarnings = new Set;
|
|
7255
7285
|
function warnUnsandboxed(ctx, message, level) {
|
|
@@ -228,17 +228,17 @@ async function main() {
|
|
|
228
228
|
const agentDir = getAgentDir();
|
|
229
229
|
const sessionManager = args.session ? SessionManager.open(args.session, args.sessionDir) : args.noSession ? SessionManager.inMemory(cwd) : SessionManager.create(cwd, args.sessionDir);
|
|
230
230
|
const absolutePaths = (paths) => paths?.map((path) => resolve(cwd, path));
|
|
231
|
-
const runtime = await createAgentSessionRuntime(async ({ cwd
|
|
231
|
+
const runtime = await createAgentSessionRuntime(async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
232
232
|
const catalog = await ModelRuntime.create({
|
|
233
233
|
credentials: new InMemoryCredentialStore,
|
|
234
234
|
allowModelNetwork: false,
|
|
235
235
|
refreshOnCreate: false
|
|
236
236
|
});
|
|
237
237
|
const services = await createAgentSessionServices({
|
|
238
|
-
cwd
|
|
238
|
+
cwd,
|
|
239
239
|
agentDir,
|
|
240
240
|
modelRuntime: catalog,
|
|
241
|
-
settingsManager: SettingsManager.create(
|
|
241
|
+
settingsManager: SettingsManager.create(cwd, agentDir, {
|
|
242
242
|
projectTrusted: args.projectTrustOverride === true
|
|
243
243
|
}),
|
|
244
244
|
extensionFlagValues: args.unknownFlags,
|
|
@@ -274,7 +274,7 @@ async function main() {
|
|
|
274
274
|
return {
|
|
275
275
|
...await createAgentSessionFromServices({
|
|
276
276
|
services,
|
|
277
|
-
sessionManager
|
|
277
|
+
sessionManager,
|
|
278
278
|
sessionStartEvent,
|
|
279
279
|
model: initial.model,
|
|
280
280
|
thinkingLevel: args.thinking,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
4
4
|
"description": "Sandboxed Bash and process-backed agents for Pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -39,22 +39,22 @@
|
|
|
39
39
|
"ci:test": "npm run test"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@landstrip/landstrip-api": "^0.19.
|
|
42
|
+
"@landstrip/landstrip-api": "^0.19.3",
|
|
43
43
|
"jsonc-parser": "^3.3.1",
|
|
44
44
|
"minimatch": "^10.2.5",
|
|
45
45
|
"undici": "^8.9.0",
|
|
46
46
|
"yaml": "^2.9.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@earendil-works/pi-ai": "^0.
|
|
49
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
50
50
|
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
51
51
|
"@earendil-works/pi-tui": "^0.85.1",
|
|
52
52
|
"@types/node": "^26.4.1",
|
|
53
|
-
"oxfmt": "^0.
|
|
53
|
+
"oxfmt": "^0.67.0",
|
|
54
54
|
"oxlint": "^1.68.0",
|
|
55
55
|
"typebox": "^1.3.6",
|
|
56
56
|
"typescript": "^5.0.0",
|
|
57
|
-
"vitest": "^
|
|
57
|
+
"vitest": "^5.0.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@earendil-works/pi-ai": ">=0.82.0",
|