@superjackfruitlabs/salaam 0.0.3 → 0.0.5
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/CHANGELOG.md +86 -0
- package/README.md +12 -11
- package/dist/src/acp-desk.js +140 -0
- package/dist/src/acp-outcome.js +182 -18
- package/dist/src/acp.js +296 -15
- package/dist/src/agent-profiles.js +45 -0
- package/dist/src/agentpod-authority.js +169 -11
- package/dist/src/agentpod-default-config.js +12 -0
- package/dist/src/agentpod-work-decision-protocol.js +40 -6
- package/dist/src/agentpod-work-decisions.js +108 -4
- package/dist/src/agentpod-work.js +1214 -127
- package/dist/src/approval-rules.js +287 -0
- package/dist/src/attach.js +3 -1
- package/dist/src/autonomy.js +80 -9
- package/dist/src/board-reads.js +106 -0
- package/dist/src/children.js +83 -0
- package/dist/src/cli-help.js +61 -6
- package/dist/src/cli.js +140 -4
- package/dist/src/command-danger.js +797 -0
- package/dist/src/command-network.js +200 -0
- package/dist/src/command-sandbox.js +327 -0
- package/dist/src/custom-commands.js +159 -0
- package/dist/src/danger-classifier.js +286 -0
- package/dist/src/execution-evidence.js +33 -1
- package/dist/src/git-helpers.js +160 -0
- package/dist/src/hooks.js +351 -0
- package/dist/src/host-bind.js +111 -0
- package/dist/src/host-breaker.js +88 -0
- package/dist/src/host-start.js +41 -1
- package/dist/src/host.js +284 -6
- package/dist/src/house-extension.js +292 -61
- package/dist/src/local-coding.js +726 -35
- package/dist/src/local-edit-helper.js +25 -0
- package/dist/src/local-edits.js +168 -6
- package/dist/src/local-undo.js +25 -16
- package/dist/src/managed.js +1 -1
- package/dist/src/mentions.js +277 -0
- package/dist/src/operator-mcp-cli.js +107 -0
- package/dist/src/operator-mcp.js +342 -0
- package/dist/src/persona.js +31 -0
- package/dist/src/pi-sessions.js +5 -1
- package/dist/src/pi.js +22 -1
- package/dist/src/project-instructions.js +195 -0
- package/dist/src/protocol.js +9 -3
- package/dist/src/rate-limit-retries.js +62 -0
- package/dist/src/recurring-work.js +1 -0
- package/dist/src/repository-snapshots.js +5 -2
- package/dist/src/run-grants.js +62 -0
- package/dist/src/service.js +482 -10
- package/dist/src/session-pick.js +77 -0
- package/dist/src/shell-passthrough.js +39 -0
- package/dist/src/station-checkpoints.js +139 -0
- package/dist/src/station-config.js +8 -1
- package/dist/src/station-files.js +20 -1
- package/dist/src/station-task-verification.js +6 -3
- package/dist/src/station-work.js +315 -22
- package/dist/src/store.js +70 -4
- package/dist/src/supervisor.js +408 -25
- package/dist/src/task-budget.js +39 -3
- package/dist/src/task-context.js +8 -2
- package/dist/src/task-critic.js +115 -0
- package/dist/src/task-driver.js +46 -2
- package/dist/src/task-policy.js +44 -12
- package/dist/src/task-verification.js +42 -9
- package/dist/src/task-work.js +47 -0
- package/dist/src/terminal-status.js +88 -0
- package/dist/src/tool-identity.js +69 -0
- package/dist/src/tui-activity.js +99 -0
- package/dist/src/tui-classifier.js +25 -0
- package/dist/src/tui-client.js +186 -5
- package/dist/src/tui-commands.js +23 -5
- package/dist/src/tui-composer.js +113 -0
- package/dist/src/tui-keymap.js +116 -0
- package/dist/src/tui-mentions.js +38 -0
- package/dist/src/tui-modals.js +157 -34
- package/dist/src/tui-notify.js +62 -0
- package/dist/src/tui-review.js +55 -0
- package/dist/src/tui-turn-keys.js +52 -0
- package/dist/src/tui-view.js +243 -69
- package/dist/src/tui.js +875 -118
- package/dist/src/turn-checkpoints.js +221 -0
- package/dist/src/turn-rewind.js +168 -0
- package/dist/src/usage.js +1 -1
- package/dist/src/web-fetch.js +114 -0
- package/dist/src/web-guard.js +163 -0
- package/dist/src/web-search.js +302 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/acp-desk.ts +155 -0
- package/src/acp-outcome.ts +174 -20
- package/src/acp.ts +386 -30
- package/src/agent-profiles.ts +48 -0
- package/src/agentpod-authority.ts +234 -15
- package/src/agentpod-default-config.ts +16 -0
- package/src/agentpod-work-decision-protocol.ts +69 -8
- package/src/agentpod-work-decisions.ts +123 -5
- package/src/agentpod-work.ts +1455 -152
- package/src/approval-rules.ts +360 -0
- package/src/attach.ts +4 -1
- package/src/autonomy.ts +111 -10
- package/src/board-reads.ts +148 -0
- package/src/children.ts +112 -0
- package/src/cli-help.ts +60 -6
- package/src/cli.ts +174 -4
- package/src/command-danger.ts +886 -0
- package/src/command-network.ts +215 -0
- package/src/command-sandbox.ts +393 -0
- package/src/custom-commands.ts +175 -0
- package/src/danger-classifier.ts +321 -0
- package/src/execution-evidence.ts +31 -1
- package/src/git-helpers.ts +177 -0
- package/src/hooks.ts +425 -0
- package/src/host-bind.ts +156 -0
- package/src/host-breaker.ts +89 -0
- package/src/host-start.ts +51 -1
- package/src/host.ts +365 -6
- package/src/house-extension.ts +313 -60
- package/src/local-coding.ts +894 -43
- package/src/local-edit-helper.ts +26 -0
- package/src/local-edits.ts +191 -8
- package/src/local-undo.ts +29 -14
- package/src/managed.ts +2 -2
- package/src/mentions.ts +304 -0
- package/src/operator-mcp-cli.ts +108 -0
- package/src/operator-mcp.ts +420 -0
- package/src/persona.ts +37 -0
- package/src/pi-sessions.ts +9 -1
- package/src/pi.ts +23 -1
- package/src/project-instructions.ts +229 -0
- package/src/protocol.ts +25 -3
- package/src/rate-limit-retries.ts +69 -0
- package/src/recurring-work.ts +1 -0
- package/src/repository-snapshots.ts +5 -2
- package/src/run-grants.ts +107 -0
- package/src/service.ts +709 -10
- package/src/session-pick.ts +102 -0
- package/src/shell-passthrough.ts +38 -0
- package/src/station-checkpoints.ts +183 -0
- package/src/station-config.ts +10 -1
- package/src/station-files.ts +20 -1
- package/src/station-task-verification.ts +6 -3
- package/src/station-work.ts +369 -20
- package/src/store.ts +70 -3
- package/src/supervisor.ts +463 -26
- package/src/task-budget.ts +48 -3
- package/src/task-context.ts +9 -2
- package/src/task-critic.ts +171 -0
- package/src/task-driver.ts +49 -2
- package/src/task-policy.ts +65 -13
- package/src/task-verification.ts +53 -9
- package/src/task-work.ts +75 -0
- package/src/terminal-status.ts +103 -0
- package/src/tool-identity.ts +110 -0
- package/src/tui-activity.ts +133 -0
- package/src/tui-classifier.ts +38 -0
- package/src/tui-client.ts +239 -8
- package/src/tui-commands.ts +49 -10
- package/src/tui-composer.ts +124 -0
- package/src/tui-keymap.ts +152 -0
- package/src/tui-mentions.ts +43 -0
- package/src/tui-modals.ts +173 -43
- package/src/tui-notify.ts +77 -0
- package/src/tui-review.ts +76 -0
- package/src/tui-turn-keys.ts +77 -0
- package/src/tui-view.ts +341 -88
- package/src/tui.ts +1047 -137
- package/src/turn-checkpoints.ts +253 -0
- package/src/turn-rewind.ts +258 -0
- package/src/usage.ts +1 -1
- package/src/web-fetch.ts +140 -0
- package/src/web-guard.ts +195 -0
- package/src/web-search.ts +402 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,92 @@ are forward-only: keep a stopped-daemon backup before upgrading.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.0.5] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
Salaam as a desk-side coding driver: an interactive terminal session in your own checkout, the same
|
|
12
|
+
session in Zed, and the approvals, sandbox and recovery to work in it daily.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Modes: Shift+Tab cycles review → accept edits → auto. `/full-access` runs commands without the
|
|
17
|
+
OS sandbox and asks only for dangerous ones; `/yolo` (typed confirmation, host setting
|
|
18
|
+
`allow_yolo_sessions`) asks for nothing and records everything.
|
|
19
|
+
- Approvals: Enter approves once, approve for the session or always in this repo, deny with a
|
|
20
|
+
reason that reaches the agent, and rules in `/permissions`. Each prompt says why it asks.
|
|
21
|
+
- Danger check: a parser-based check of commands (it unwraps `sudo`, `env`, `xargs`, `sh -c` and
|
|
22
|
+
more; anything unreadable is dangerous), plus an optional small-model classifier that can only
|
|
23
|
+
make a command ask (`/classifier`, off by default, hosts can pin or forbid it).
|
|
24
|
+
- Run one command outside the sandbox: a command may ask, saying why. It always asks except in
|
|
25
|
+
YOLO; auto mode and repo rules never approve it.
|
|
26
|
+
- Listening: a command you approve to listen may serve a dev server; macOS cannot limit it to
|
|
27
|
+
localhost, and the approval says so. Refused on Linux.
|
|
28
|
+
- Pushes and pull requests always ask, except in YOLO or your own rule.
|
|
29
|
+
- Rewind to any turn (files, conversation or both) and a per-file review of changes.
|
|
30
|
+
- `@` mentions attach files and images; background commands (`/jobs`) and desk notifications.
|
|
31
|
+
- Git helpers `/commit`, `/branch`, `/pr`, `/diff`; your own slash commands; hooks (a repo's hooks
|
|
32
|
+
run only after you trust them).
|
|
33
|
+
- Terminal: compact tool lines with Ctrl+X for the full transcript, a progress line with Esc to
|
|
34
|
+
stop, Ctrl+C to clear, Ctrl+R history, Ctrl+G editor, readline keys, `!command`, `salaam -c`,
|
|
35
|
+
a session picker, the `?` key sheet and the window title.
|
|
36
|
+
- Zed and other ACP editors: `salaam acp --local` opens your desk session in the project folder,
|
|
37
|
+
with the mode picker, approvals that show diffs and offer Allow for this session, finished edits
|
|
38
|
+
as diffs, and `/rewind` for files.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Salaam starts in Zed when you have MCP servers configured: the desk session ignores the editor's
|
|
43
|
+
own MCP servers instead of refusing them.
|
|
44
|
+
- Tool calls show in the turn they belong to, and the full transcript shows command output as text.
|
|
45
|
+
- Usage counts cached input and the whole cost.
|
|
46
|
+
- Choosing a model that needs a new session opens one with that model instead of the old one.
|
|
47
|
+
- A failed session shows its error; leftover background commands are stopped after a restart on
|
|
48
|
+
macOS.
|
|
49
|
+
|
|
50
|
+
## [0.0.4] - 2026-10-30
|
|
51
|
+
|
|
52
|
+
Board work on a protected AgentPod station, made to match the Hermes workflow, plus a sandbox
|
|
53
|
+
for local commands.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- Approvals: "Approve for this run", per-agent "Always allow" (offered on workspace writes, owners
|
|
58
|
+
and admins only), plain-words approval offers, and rules added at renewal joining the run in
|
|
59
|
+
flight.
|
|
60
|
+
- Completion critic: a plan may declare a rubric; a read-only reviewer judges the verified
|
|
61
|
+
deliverables before the task completes. Completion hooks are required checks.
|
|
62
|
+
- Checkpoints: read-only checkpoints of a station run, their diff against the live workspace, and
|
|
63
|
+
restore exactly as approved.
|
|
64
|
+
- Command sandbox: bubblewrap on Linux and Seatbelt on macOS. With no usable sandbox, local
|
|
65
|
+
commands are refused unless the host config sets `unsandboxed_commands: "allow"`.
|
|
66
|
+
- Command network through the broker: with `command_network: "broker"` in the host config, local
|
|
67
|
+
commands reach only the hosts the operator lists in the stk policy (`local-commands`
|
|
68
|
+
egress `hosts`). Everything direct stays blocked, and each command's result counts the hosts it
|
|
69
|
+
reached or was refused. Needs SuperToolkit v0.0.4.
|
|
70
|
+
- Board parity: the work-status tool-step tail carries each step's output and a readable title;
|
|
71
|
+
plan updates reach the card; board reads are served by the hub.
|
|
72
|
+
- Operator persona pinned by digest in the host manifest.
|
|
73
|
+
- Budgets and retries: step request budgets, rate-limit retry then a provable stop, and
|
|
74
|
+
verification-budget stops that settle without anyone clearing them.
|
|
75
|
+
- A fetch host that keeps failing is paused briefly instead of failing the run.
|
|
76
|
+
- Add `--tools-user` (`SALAAM_TOOLS_USER`) to run local agent commands, edits and
|
|
77
|
+
undo as a separate unprivileged OS user via a runas-restricted sudo rule, so
|
|
78
|
+
tools cannot read the provider credential. Without it, startup warns.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- A verification read refused at spawn no longer leaves the daemon unable to start; a stuck
|
|
83
|
+
pid-less helper settles once `/proc` proves no helper runs (Linux only; elsewhere it stays
|
|
84
|
+
fenced).
|
|
85
|
+
- A run stopped by its own failed acceptance check reaches the board as a failed run instead of
|
|
86
|
+
parking.
|
|
87
|
+
- Readable titles for MCP steps with no input, and for finished steps.
|
|
88
|
+
|
|
89
|
+
### Upgrade notes
|
|
90
|
+
|
|
91
|
+
- Hosts without a usable bubblewrap (Linux) or Seatbelt (macOS) now refuse local commands. Install
|
|
92
|
+
or repair the sandbox, or set `unsandboxed_commands: "allow"` explicitly.
|
|
93
|
+
- Release after AgentPod v0.1.118 (hub): the step output field and board reads need that hub.
|
|
94
|
+
|
|
9
95
|
## [0.0.3] - 2026-10-22
|
|
10
96
|
|
|
11
97
|
- Update the MCP SDK and optional Hermes certificate bundle to patched releases.
|
package/README.md
CHANGED
|
@@ -99,17 +99,18 @@ by OS permissions. See [providers and credentials](docs/providers.md).
|
|
|
99
99
|
|
|
100
100
|
## What stays with the work
|
|
101
101
|
|
|
102
|
-
| Capability | What it means
|
|
103
|
-
| ------------------------------------------------ |
|
|
104
|
-
| [Sessions](docs/service-reference.md) | Durable dispatch, client outboxes, controller leases and committed event replay.
|
|
105
|
-
| [Coding](docs/coding.md) | Owned workspace copies, hash-bound patches and isolated commands through a proven Docker backend.
|
|
106
|
-
| [Tasks](docs/durable-tasks.md) | Stable steps, checked plans, budgets, checkpoints and opt-in bounded progression. Model text cannot complete a checked task.
|
|
107
|
-
| [Specialists and teams](docs/agents.md) | Admitted profiles, isolated coding proposals, explicit integration, durable task-step claims and passive messages.
|
|
108
|
-
| [External harnesses](docs/external-harnesses.md) | Pinned Gemini, Claude ACP and Codex workers with explicit invocation limits and shared approvals.
|
|
109
|
-
| [A2A peers](docs/a2a.md) | Authenticated, bounded peer analysis, explicit inbound grants and durable task ownership.
|
|
110
|
-
| [Jobs and schedules](docs/jobs.md) | Occurrence deduplication, immutable recurring templates and explicit missed-run and timezone/DST policies.
|
|
111
|
-
| [Host operation](docs/host-operation.md) | Reviewed runtime/backend manifests, bounded supervisor restart and a shared CLI/TUI/ACP inbox.
|
|
112
|
-
| [Clients](docs/clients.md) | Terminal attachment, SSH and ACP v1 against the same service.
|
|
102
|
+
| Capability | What it means |
|
|
103
|
+
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| [Sessions](docs/service-reference.md) | Durable dispatch, client outboxes, controller leases and committed event replay. |
|
|
105
|
+
| [Coding](docs/coding.md) | Owned workspace copies, hash-bound patches and isolated commands through a proven Docker backend. |
|
|
106
|
+
| [Tasks](docs/durable-tasks.md) | Stable steps, checked plans, budgets, checkpoints and opt-in bounded progression. Model text cannot complete a checked task. |
|
|
107
|
+
| [Specialists and teams](docs/agents.md) | Admitted profiles, isolated coding proposals, explicit integration, durable task-step claims and passive messages. |
|
|
108
|
+
| [External harnesses](docs/external-harnesses.md) | Pinned Gemini, Claude ACP and Codex workers with explicit invocation limits and shared approvals. |
|
|
109
|
+
| [A2A peers](docs/a2a.md) | Authenticated, bounded peer analysis, explicit inbound grants and durable task ownership. |
|
|
110
|
+
| [Jobs and schedules](docs/jobs.md) | Occurrence deduplication, immutable recurring templates and explicit missed-run and timezone/DST policies. |
|
|
111
|
+
| [Host operation](docs/host-operation.md) | Reviewed runtime/backend manifests, bounded supervisor restart and a shared CLI/TUI/ACP inbox. |
|
|
112
|
+
| [Clients](docs/clients.md) | Terminal attachment, SSH and ACP v1 against the same service. |
|
|
113
|
+
| [MCP servers](docs/mcp.md) | Operator-declared stdio and HTTP servers (`sal mcp add`); workspaces never add tools; no inline secrets; per-session provenance. |
|
|
113
114
|
|
|
114
115
|
## Read this before trusting it
|
|
115
116
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { isAbsolute, join } from 'node:path';
|
|
2
|
+
import { relativeTime } from './session-pick.js';
|
|
3
|
+
const MODES = [
|
|
4
|
+
{ id: 'review', name: 'Review', description: 'Asks before every edit and command' },
|
|
5
|
+
{
|
|
6
|
+
id: 'accept-edits',
|
|
7
|
+
name: 'Accept edits',
|
|
8
|
+
description: 'Edits in the project apply at once; commands ask',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: 'auto',
|
|
12
|
+
name: 'Auto',
|
|
13
|
+
description: 'Project edits and sandboxed commands run at once; the rest asks',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'full-access',
|
|
17
|
+
name: 'Full access',
|
|
18
|
+
description: 'No OS sandbox; only dangerous commands ask',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
const YOLO = {
|
|
22
|
+
id: 'yolo',
|
|
23
|
+
name: 'YOLO',
|
|
24
|
+
description: 'No approvals at all and no sandbox. Turn it off in the terminal with /yolo',
|
|
25
|
+
};
|
|
26
|
+
export function deskMode(policy) {
|
|
27
|
+
if (policy.access === 'yolo')
|
|
28
|
+
return 'yolo';
|
|
29
|
+
if (policy.access === 'full')
|
|
30
|
+
return 'full-access';
|
|
31
|
+
return policy.mode;
|
|
32
|
+
}
|
|
33
|
+
/** `modes` and the matching `configOptions`, kept in step (ACP asks agents to send both). */
|
|
34
|
+
export function modeState(p) {
|
|
35
|
+
const current = deskMode(p.policy);
|
|
36
|
+
const list = MODES.filter((m) => m.id !== 'full-access' || p.full_access_allowed !== false);
|
|
37
|
+
if (current === 'yolo')
|
|
38
|
+
list.push(YOLO);
|
|
39
|
+
else if (!list.some((m) => m.id === current))
|
|
40
|
+
list.unshift({ id: current, name: current, description: 'Set in the terminal' });
|
|
41
|
+
return {
|
|
42
|
+
modes: { currentModeId: current, availableModes: list },
|
|
43
|
+
configOptions: [
|
|
44
|
+
{
|
|
45
|
+
id: 'mode',
|
|
46
|
+
name: 'Mode',
|
|
47
|
+
category: 'mode',
|
|
48
|
+
type: 'select',
|
|
49
|
+
currentValue: current,
|
|
50
|
+
options: list.map((m) => ({ value: m.id, name: m.name, description: m.description })),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The policy a chosen mode asks for. Picking a mode turns full access off; full access runs in
|
|
56
|
+
* auto, so edits and commands go ahead and only dangerous commands ask. */
|
|
57
|
+
export function policyFor(mode, allowed = {}) {
|
|
58
|
+
if (mode === 'review' || mode === 'accept-edits' || mode === 'auto')
|
|
59
|
+
return { mode, paths: [] };
|
|
60
|
+
if (mode === 'full-access') {
|
|
61
|
+
if (allowed.full === false)
|
|
62
|
+
throw new Error('The operator does not allow full access on this host. Commands stay in the OS sandbox.');
|
|
63
|
+
return { mode: 'auto', paths: [], access: 'full' };
|
|
64
|
+
}
|
|
65
|
+
if (mode === 'yolo')
|
|
66
|
+
throw new Error('YOLO is turned on only in the terminal: run /yolo there and type yolo.');
|
|
67
|
+
throw new Error('Unknown mode ' + JSON.stringify(mode));
|
|
68
|
+
}
|
|
69
|
+
export const FULL_ACCESS_WARNING = 'Later commands in this session run WITHOUT the OS sandbox: they can read and change any file your user can, and reach the network directly. They run without asking, except commands the danger check calls dangerous (force pushes, deletes, publishing, credentials, anything it cannot read), which still ask and say why. Pick another mode to turn it off.';
|
|
70
|
+
/** ACP diff content for an edit request: absolute paths, null old text for a new file. */
|
|
71
|
+
export function editDiffs(root, edits) {
|
|
72
|
+
return edits.map((e) => ({
|
|
73
|
+
type: 'diff',
|
|
74
|
+
path: isAbsolute(e.path) ? e.path : join(root, e.path),
|
|
75
|
+
oldText: e.before,
|
|
76
|
+
newText: e.after,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/** Why an approval asks, from the reviewed text and the mode, in one line. */
|
|
80
|
+
export function askReason(value, mode) {
|
|
81
|
+
const danger = /^Dangerous \(([^)]+)\): (.*)$/m.exec(value);
|
|
82
|
+
if (danger)
|
|
83
|
+
return `Asks because this command is dangerous (${danger[1]}): ${danger[2]}`;
|
|
84
|
+
const outside = /^Outside the sandbox: (.*?)\. This one command/m.exec(value);
|
|
85
|
+
if (outside)
|
|
86
|
+
return `Asks because it wants to run outside the sandbox: ${outside[1]}`;
|
|
87
|
+
if (/^Publishes: /m.test(value))
|
|
88
|
+
return 'Asks because pushing and opening pull requests always ask.';
|
|
89
|
+
if (/^May listen: /m.test(value))
|
|
90
|
+
return 'Asks because this command may accept network connections; macOS cannot limit it to localhost.';
|
|
91
|
+
const edit = /^(---|\+\+\+) /m.test(value) && !/\nCommand:\n/.test(value);
|
|
92
|
+
if (mode === 'review')
|
|
93
|
+
return 'Asks because review mode asks before every edit and command.';
|
|
94
|
+
if (mode === 'accept-edits' && !edit)
|
|
95
|
+
return 'Asks because accept edits still asks before commands.';
|
|
96
|
+
if (mode === 'auto')
|
|
97
|
+
return edit
|
|
98
|
+
? 'Asks because this edit is outside what auto mode approves on its own.'
|
|
99
|
+
: 'Asks because auto mode approves only sandboxed commands it can read.';
|
|
100
|
+
return 'Asks because nothing approves this request on its own.';
|
|
101
|
+
}
|
|
102
|
+
export const DESK_COMMANDS = [
|
|
103
|
+
{
|
|
104
|
+
name: 'rewind',
|
|
105
|
+
description: 'Put the files back as they were before an earlier turn',
|
|
106
|
+
input: { hint: 'turn number (leave empty to list turns)' },
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
/** `/rewind`, `/rewind 3`. Anything else is not a desk command and goes to the model. */
|
|
110
|
+
export function parseDeskCommand(text) {
|
|
111
|
+
const match = /^\/rewind(?:\s+(\S+))?\s*$/.exec(text.trim());
|
|
112
|
+
if (!match)
|
|
113
|
+
return null;
|
|
114
|
+
if (match[1] === undefined)
|
|
115
|
+
return { command: 'rewind' };
|
|
116
|
+
const turn = Number(match[1]);
|
|
117
|
+
if (!Number.isSafeInteger(turn) || turn < 1)
|
|
118
|
+
throw new Error('Use /rewind with a turn number from the list, e.g. /rewind 3.');
|
|
119
|
+
return { command: 'rewind', turn };
|
|
120
|
+
}
|
|
121
|
+
export function checkpointList(points, now = Date.now()) {
|
|
122
|
+
if (!points.length)
|
|
123
|
+
return 'No turns to go back to yet.';
|
|
124
|
+
return [
|
|
125
|
+
'Turns you can go back to (the files as they were before each turn ran):',
|
|
126
|
+
'',
|
|
127
|
+
...points
|
|
128
|
+
.slice()
|
|
129
|
+
.reverse()
|
|
130
|
+
.map((p) => `- Turn ${p.turn} · ${relativeTime(p.at, now)} · ${p.text.split('\n')[0].slice(0, 80)}`),
|
|
131
|
+
'',
|
|
132
|
+
'Run /rewind followed by a turn number to review and restore its files.',
|
|
133
|
+
].join('\n');
|
|
134
|
+
}
|
|
135
|
+
/** Zed cannot follow a rewind into a new session, so from the editor only files are restored. */
|
|
136
|
+
export const REWIND_CHOICES = [
|
|
137
|
+
{ optionId: 'files', name: 'Restore files', kind: 'allow_once' },
|
|
138
|
+
{ optionId: 'cancel', name: 'Cancel', kind: 'reject_once' },
|
|
139
|
+
];
|
|
140
|
+
export const CONVERSATION_REWIND_NOTE = 'To go back in the conversation too, run /rewind in the terminal (salaam -c opens this session): it continues in a new session.';
|
package/dist/src/acp-outcome.js
CHANGED
|
@@ -243,6 +243,139 @@ export function acpFailure(error, context = {}) {
|
|
|
243
243
|
...(kind === 'unknown' ? {} : { retryable: false }),
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
|
+
/** ACP tool kind for an approval, so a client's modes can tell edits apart:
|
|
247
|
+
* AgentPod's accept-edits approves only kind "edit"; full-auto approves all; ask none. */
|
|
248
|
+
export function permissionKind(operationKind) {
|
|
249
|
+
if (operationKind === 'workspace')
|
|
250
|
+
return 'edit';
|
|
251
|
+
if (operationKind === 'harness')
|
|
252
|
+
return 'execute';
|
|
253
|
+
return 'other';
|
|
254
|
+
}
|
|
255
|
+
/** An operator MCP catalog read is a read; a server's `fetch` tool is a fetch; others stay other. */
|
|
256
|
+
function operatorMcpKind(action, tool) {
|
|
257
|
+
if (action === 'catalog')
|
|
258
|
+
return 'read';
|
|
259
|
+
return action === 'call' && tool === 'fetch' ? 'fetch' : 'other';
|
|
260
|
+
}
|
|
261
|
+
function toolArgs(input) {
|
|
262
|
+
try {
|
|
263
|
+
const value = typeof input === 'string' ? JSON.parse(input) : input;
|
|
264
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
catch { }
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
const label = (v) => (typeof v === 'string' && /^[\w.-]{1,128}$/.test(v) ? v : undefined);
|
|
271
|
+
/** One line of what a tool acted on, for a card title: no control characters, at most `max`. */
|
|
272
|
+
function shown(v, max) {
|
|
273
|
+
if (typeof v !== 'string')
|
|
274
|
+
return undefined;
|
|
275
|
+
const t = v
|
|
276
|
+
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
277
|
+
.replace(/\s+/g, ' ')
|
|
278
|
+
.trim();
|
|
279
|
+
if (!t)
|
|
280
|
+
return undefined;
|
|
281
|
+
return t.length > max ? t.slice(0, max - 1) + '…' : t;
|
|
282
|
+
}
|
|
283
|
+
/** A URL as host and path only: never its login, query or fragment (they may carry a secret). */
|
|
284
|
+
function shownUrl(v) {
|
|
285
|
+
if (typeof v !== 'string')
|
|
286
|
+
return undefined;
|
|
287
|
+
try {
|
|
288
|
+
const u = new URL(v);
|
|
289
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
290
|
+
return undefined;
|
|
291
|
+
return u.host + (u.pathname === '/' ? '' : u.pathname);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const titled = (verb, what) => {
|
|
298
|
+
if (!what)
|
|
299
|
+
return verb;
|
|
300
|
+
const t = `${verb}: ${what}`;
|
|
301
|
+
return t.length > 80 ? t.slice(0, 79) + '…' : t;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* The ACP title for a tool call, the way a board agent's card reads: the tool and what it acted on
|
|
305
|
+
* ("fetch: host/path", "write: runs/r/answer.md"). Every operator MCP call goes through one wrapper
|
|
306
|
+
* tool, so it is titled by the MCP tool itself. Only plain identifiers, URL host and path, paths,
|
|
307
|
+
* queries and commands are echoed, clipped to one line of 80 characters.
|
|
308
|
+
*/
|
|
309
|
+
/** A clipped input no longer parses: read the leading plain string fields it still carries. */
|
|
310
|
+
function clippedArgs(input) {
|
|
311
|
+
if (typeof input !== 'string')
|
|
312
|
+
return {};
|
|
313
|
+
const out = {};
|
|
314
|
+
for (const key of [
|
|
315
|
+
'action',
|
|
316
|
+
'path',
|
|
317
|
+
'query',
|
|
318
|
+
'pattern',
|
|
319
|
+
'command',
|
|
320
|
+
'server',
|
|
321
|
+
'tool',
|
|
322
|
+
'item_id',
|
|
323
|
+
]) {
|
|
324
|
+
const m = new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.){0,512})"`).exec(input);
|
|
325
|
+
if (m) {
|
|
326
|
+
try {
|
|
327
|
+
out[key] = JSON.parse(`"${m[1]}"`);
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// A clipped patch is a long one, so a write: a delete (content null) is short and never clipped.
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
export function toolTitle(name, input) {
|
|
336
|
+
const parsed = toolArgs(input);
|
|
337
|
+
const args = Object.keys(parsed).length ? parsed : clippedArgs(input);
|
|
338
|
+
const action = typeof args.action === 'string' ? args.action : '';
|
|
339
|
+
if (name === 'salaam_operator_mcp') {
|
|
340
|
+
const server = label(args.server);
|
|
341
|
+
const tool = label(args.tool);
|
|
342
|
+
if (action === 'call' && server && tool) {
|
|
343
|
+
const a = toolArgs(args.arguments);
|
|
344
|
+
return titled(tool, shownUrl(a.url) ?? shown(a.query ?? a.path ?? a.pattern, 80));
|
|
345
|
+
}
|
|
346
|
+
if (action === 'catalog')
|
|
347
|
+
return titled('MCP catalog', server);
|
|
348
|
+
if (action === 'call')
|
|
349
|
+
return titled('MCP call', server ?? tool);
|
|
350
|
+
return action ? titled('MCP request', label(action)) : 'MCP request (no input)';
|
|
351
|
+
}
|
|
352
|
+
if (name === 'salaam_board') {
|
|
353
|
+
if (action === 'card')
|
|
354
|
+
return 'read this card';
|
|
355
|
+
if (action === 'attempts')
|
|
356
|
+
return 'read this card’s runs';
|
|
357
|
+
if (action === 'library_search')
|
|
358
|
+
return titled('library_search', shown(args.query, 80));
|
|
359
|
+
if (action === 'library_get')
|
|
360
|
+
return titled('library_get', shown(args.item_id, 32));
|
|
361
|
+
return name;
|
|
362
|
+
}
|
|
363
|
+
if (['salaam_station', 'salaam_files', 'salaam_repository'].includes(name)) {
|
|
364
|
+
if (action === 'patch')
|
|
365
|
+
return titled(args.content === null ? 'delete' : 'write', shown(args.path, 80));
|
|
366
|
+
if (action === 'inspect' && args.path === undefined)
|
|
367
|
+
return 'inspect workspace';
|
|
368
|
+
if (action === 'check')
|
|
369
|
+
return titled('check', shown(args.command, 80));
|
|
370
|
+
if (['read', 'list', 'inspect', 'diff'].includes(action))
|
|
371
|
+
return titled(action, shown(args.path, 80));
|
|
372
|
+
if (action === 'search')
|
|
373
|
+
return titled('search', shown(args.query ?? args.pattern, 80));
|
|
374
|
+
if (action)
|
|
375
|
+
return action;
|
|
376
|
+
}
|
|
377
|
+
return name;
|
|
378
|
+
}
|
|
246
379
|
export function toolMetadata(name, input, root) {
|
|
247
380
|
let args = {};
|
|
248
381
|
try {
|
|
@@ -252,25 +385,33 @@ export function toolMetadata(name, input, root) {
|
|
|
252
385
|
}
|
|
253
386
|
catch { }
|
|
254
387
|
const action = String(args.action ?? '');
|
|
255
|
-
const kind = name === '
|
|
256
|
-
?
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
? '
|
|
260
|
-
: '
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
: ['salaam_files', 'salaam_repository', 'salaam_station'].includes(name) &&
|
|
266
|
-
['search', 'list'].includes(action)
|
|
267
|
-
? 'search'
|
|
268
|
-
: ['salaam_files', 'salaam_repository', 'salaam_station'].includes(name) &&
|
|
269
|
-
['read', 'inspect', ...(name === 'salaam_repository' ? ['diff'] : [])].includes(action)
|
|
270
|
-
? 'read'
|
|
271
|
-
: ['salaam_history', 'inspect_child', 'consume_child_result'].includes(name)
|
|
388
|
+
const kind = name === 'salaam_board'
|
|
389
|
+
? 'read'
|
|
390
|
+
: name === 'salaam_local' && ['edit', 'command'].includes(action)
|
|
391
|
+
? action === 'edit'
|
|
392
|
+
? 'edit'
|
|
393
|
+
: 'execute'
|
|
394
|
+
: name === 'salaam_command' || (name === 'salaam_station' && action === 'check')
|
|
395
|
+
? ['start', 'cancel', 'check'].includes(action)
|
|
396
|
+
? 'execute'
|
|
397
|
+
: ['status', 'logs'].includes(action)
|
|
272
398
|
? 'read'
|
|
273
|
-
: 'other'
|
|
399
|
+
: 'other'
|
|
400
|
+
: ['salaam_files', 'salaam_station'].includes(name) && action === 'patch'
|
|
401
|
+
? args.content === null
|
|
402
|
+
? 'delete'
|
|
403
|
+
: 'edit'
|
|
404
|
+
: ['salaam_files', 'salaam_repository', 'salaam_station'].includes(name) &&
|
|
405
|
+
['search', 'list'].includes(action)
|
|
406
|
+
? 'search'
|
|
407
|
+
: ['salaam_files', 'salaam_repository', 'salaam_station'].includes(name) &&
|
|
408
|
+
['read', 'inspect', ...(name === 'salaam_repository' ? ['diff'] : [])].includes(action)
|
|
409
|
+
? 'read'
|
|
410
|
+
: ['salaam_history', 'inspect_child', 'consume_child_result'].includes(name)
|
|
411
|
+
? 'read'
|
|
412
|
+
: name === 'salaam_operator_mcp'
|
|
413
|
+
? operatorMcpKind(action, args.tool)
|
|
414
|
+
: 'other';
|
|
274
415
|
const locations = [];
|
|
275
416
|
if (['salaam_files', 'salaam_station'].includes(name) &&
|
|
276
417
|
root &&
|
|
@@ -286,3 +427,26 @@ export function toolMetadata(name, input, root) {
|
|
|
286
427
|
}
|
|
287
428
|
return { kind, locations };
|
|
288
429
|
}
|
|
430
|
+
/** A durable task's steps as the ACP `plan` session update: the whole list, replaced on every
|
|
431
|
+
* change (https://agentclientprotocol.com/protocol/agent-plan). ACP has only pending,
|
|
432
|
+
* in_progress and completed, so a step that stopped says so in its text instead. */
|
|
433
|
+
export function planUpdate(task) {
|
|
434
|
+
if (!task?.steps.length)
|
|
435
|
+
return null;
|
|
436
|
+
return {
|
|
437
|
+
sessionUpdate: 'plan',
|
|
438
|
+
entries: task.steps.map((step) => {
|
|
439
|
+
const status = step.state === 'completed'
|
|
440
|
+
? 'completed'
|
|
441
|
+
: step.state === 'running' || step.state === 'awaiting_review'
|
|
442
|
+
? 'in_progress'
|
|
443
|
+
: 'pending';
|
|
444
|
+
const stopped = ['cancelled', 'failed', 'interrupted'].includes(step.state);
|
|
445
|
+
return {
|
|
446
|
+
content: stopped ? `${step.title} (${step.state})` : step.title,
|
|
447
|
+
priority: 'medium',
|
|
448
|
+
status,
|
|
449
|
+
};
|
|
450
|
+
}),
|
|
451
|
+
};
|
|
452
|
+
}
|