@songsid/agend 2.1.6-beta.3 → 2.1.6-beta.4

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.
@@ -10,7 +10,17 @@ roles: [worker]
10
10
 
11
11
  - Treat the assignment as the full scope unless the sender explicitly expands it.
12
12
  - Work without acknowledgment-only messages. Silence means the task is in progress.
13
- - Do not call `delegate_task`; delegation and fleet orchestration belong to General.
13
+ - Fleet orchestration is not yours: you do not have `create_instance`,
14
+ `delete_instance`, `restart_instance`, `deploy_template`, the team tools, or
15
+ the tools that create, change or delete schedules. Calling one is refused
16
+ with an explanation rather than silently ignored. If the work needs more
17
+ capacity or another agent restarted, say so with `report_result` and let the
18
+ coordinator decide.
19
+ - You can still *look*: `list_schedules`, `list_deployments`, `list_instances`
20
+ and the other read-only queries are yours, so check before asking.
21
+ - You *can* `delegate_task`, because it only hands work to an instance that
22
+ already exists. Prefer reporting back over delegating sideways unless the
23
+ assignment said otherwise — delegation belongs to whoever is coordinating.
14
24
 
15
25
  ## Request missing information
16
26
 
@@ -0,0 +1,38 @@
1
+ export interface InstanceToolUse {
2
+ readonly instance: string;
3
+ /** Tool name → how many times this instance called it. */
4
+ readonly tools: ReadonlyMap<string, number>;
5
+ }
6
+ export interface NoticeInput {
7
+ readonly defaultsToolSet?: string;
8
+ readonly instances: Readonly<Record<string, {
9
+ tool_set?: string;
10
+ general_topic?: boolean;
11
+ }>>;
12
+ /** Recent tool use, from the activity log. Empty is a valid answer. */
13
+ readonly recent: readonly InstanceToolUse[];
14
+ }
15
+ export interface CoordinatorCandidate {
16
+ readonly instance: string;
17
+ /** Only the tools the new profile would refuse, most-used first. */
18
+ readonly refusedTools: ReadonlyArray<readonly [string, number]>;
19
+ }
20
+ /**
21
+ * Which instances would lose something they have actually been using.
22
+ *
23
+ * The rule is "called a tool the resolved profile refuses" — that is the
24
+ * definition of breaking, not a guess at intent. An instance that only ever
25
+ * called `delegate_task` does not appear, because `delegate_task` stays with
26
+ * the worker and nothing about it changes.
27
+ */
28
+ export declare function coordinatorCandidates(input: NoticeInput): CoordinatorCandidate[];
29
+ /** True when this fleet asked for `full` out loud, so the new default cannot reach it. */
30
+ export declare function hasExplicitFullDefault(input: NoticeInput): boolean;
31
+ export declare function instancesExplicitlyFull(input: NoticeInput): string[];
32
+ /**
33
+ * The whole notice, or null when there is nothing worth saying.
34
+ *
35
+ * One message rather than two: an operator who needs to hear both hears them
36
+ * together, in the order they have to act on them.
37
+ */
38
+ export declare function buildToolPermissionsNotice(input: NoticeInput): string | null;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * What to tell an operator whose fleet predates the `worker` default.
3
+ *
4
+ * Two things can be true of an existing installation and neither of them is
5
+ * something to fix on the operator's behalf:
6
+ *
7
+ * - `defaults.tool_set: full` may be written in their `fleet.yaml`, in which
8
+ * case the new code default never applies and every worker still holds the
9
+ * whole toolbox. Rewriting that line would be a surprise placed inside their
10
+ * own file.
11
+ * - Some instances really are coordinating, and marking them is a judgement
12
+ * about how their fleet is organised.
13
+ *
14
+ * So this computes the advice and says it once, naming the instances that will
15
+ * actually break rather than the ones that merely look like coordinators. A
16
+ * notice that asks someone to mark twenty instances they did not need to mark
17
+ * is a notice they stop reading.
18
+ */
19
+ import { mayUseTool, resolveToolSet } from "./tool-permissions.js";
20
+ /**
21
+ * Which instances would lose something they have actually been using.
22
+ *
23
+ * The rule is "called a tool the resolved profile refuses" — that is the
24
+ * definition of breaking, not a guess at intent. An instance that only ever
25
+ * called `delegate_task` does not appear, because `delegate_task` stays with
26
+ * the worker and nothing about it changes.
27
+ */
28
+ export function coordinatorCandidates(input) {
29
+ const out = [];
30
+ for (const use of input.recent) {
31
+ // The activity log outlives the config. An instance that has since been
32
+ // deleted resolves to `worker` like any other unknown name, and would be
33
+ // named in a list of things to go and edit that no longer exist.
34
+ if (!Object.hasOwn(input.instances, use.instance))
35
+ continue;
36
+ const config = input.instances[use.instance];
37
+ // An instance that is already explicitly widened, or is a general, is not
38
+ // about to lose anything.
39
+ const profile = resolveToolSet(config, use.instance);
40
+ const refused = [...use.tools.entries()]
41
+ .filter(([tool]) => !mayUseTool(profile, tool))
42
+ .sort((a, b) => b[1] - a[1]);
43
+ if (refused.length > 0)
44
+ out.push({ instance: use.instance, refusedTools: refused });
45
+ }
46
+ return out.sort((a, b) => b.refusedTools.reduce((n, [, c]) => n + c, 0) - a.refusedTools.reduce((n, [, c]) => n + c, 0));
47
+ }
48
+ /** True when this fleet asked for `full` out loud, so the new default cannot reach it. */
49
+ export function hasExplicitFullDefault(input) {
50
+ return input.defaultsToolSet === "full";
51
+ }
52
+ export function instancesExplicitlyFull(input) {
53
+ return Object.entries(input.instances)
54
+ .filter(([, config]) => config?.tool_set === "full")
55
+ .map(([name]) => name)
56
+ .sort();
57
+ }
58
+ /**
59
+ * The whole notice, or null when there is nothing worth saying.
60
+ *
61
+ * One message rather than two: an operator who needs to hear both hears them
62
+ * together, in the order they have to act on them.
63
+ */
64
+ export function buildToolPermissionsNotice(input) {
65
+ const lines = [];
66
+ const explicitFull = hasExplicitFullDefault(input);
67
+ const alsoFull = instancesExplicitlyFull(input);
68
+ const candidates = coordinatorCandidates(input);
69
+ if (explicitFull) {
70
+ lines.push("Your fleet.yaml sets `defaults.tool_set: full`, so every agent still gets all of AgEnD's tools —", "including create_instance, delete_instance and update_fleet_defaults. AgEnD's own default is now", "`worker`, but an explicit setting wins and yours has not been changed.", "", "To take the new default, remove that line (or set it to `worker`).");
71
+ }
72
+ else if (alsoFull.length > 0) {
73
+ lines.push(`These instances are set to \`tool_set: full\` and keep every tool: ${alsoFull.join(", ")}.`, "AgEnD's default is now `worker`; an explicit setting wins and yours have not been changed.");
74
+ }
75
+ if (candidates.length > 0) {
76
+ if (lines.length > 0)
77
+ lines.push("");
78
+ lines.push(`${candidates.length} instance${candidates.length === 1 ? " has" : "s have"} used tools a worker does not get.`, "Mark them `tool_set: coordinator` so they keep working:", "");
79
+ for (const c of candidates) {
80
+ const used = c.refusedTools.map(([tool, count]) => `${tool}×${count}`).join(" ");
81
+ lines.push(` ${c.instance} ${used}`);
82
+ }
83
+ lines.push("",
84
+ // The derivation only sees one of the three paths a tool call can take,
85
+ // so this says what it found rather than claiming it found everything.
86
+ "That list is what recent activity shows; an agent that has been quiet may still need marking.");
87
+ }
88
+ if (lines.length === 0)
89
+ return null;
90
+ return ["AgEnD tool permissions have changed.", "", ...lines].join("\n");
91
+ }
92
+ //# sourceMappingURL=tool-permissions-notice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-permissions-notice.js","sourceRoot":"","sources":["../src/tool-permissions-notice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAoB,MAAM,uBAAuB,CAAC;AAqBrF;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAkB;IACtD,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAC/B,wEAAwE;QACxE,yEAAyE;QACzE,iEAAiE;QACjE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,QAAQ,CAAC;YAAE,SAAS;QAC5D,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7C,0EAA0E;QAC1E,0BAA0B;QAC1B,MAAM,OAAO,GAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClE,MAAM,OAAO,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;aACrC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;aAC9C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACvB,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,sBAAsB,CAAC,KAAkB;IACvD,OAAO,KAAK,CAAC,eAAe,KAAK,MAAM,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAkB;IACxD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;SACnC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,MAAM,CAAC;SACnD,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC;SACrB,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAkB;IAC3D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,YAAY,GAAG,sBAAsB,CAAC,KAAK,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAEhD,IAAI,YAAY,EAAE,CAAC;QACjB,KAAK,CAAC,IAAI,CACR,kGAAkG,EAClG,kGAAkG,EAClG,wEAAwE,EACxE,EAAE,EACF,oEAAoE,CACrE,CAAC;IACJ,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CACR,sEAAsE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAC5F,4FAA4F,CAC7F,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrC,KAAK,CAAC,IAAI,CACR,GAAG,UAAU,CAAC,MAAM,YAAY,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,oCAAoC,EAC/G,yDAAyD,EACzD,EAAE,CACH,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,MAAM,IAAI,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,KAAK,CAAC,IAAI,CACR,EAAE;QACF,wEAAwE;QACxE,uEAAuE;QACvE,+FAA+F,CAChG,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,CAAC,sCAAsC,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,93 @@
1
+ export type ToolSetName = "full" | "standard" | "worker" | "coordinator" | "minimal" | "general";
2
+ /**
3
+ * Three tiers plus the three that already existed.
4
+ *
5
+ * `general` is not `coordinator` plus channel I/O — it is deliberately smaller:
6
+ * a dispatcher has no business deleting instances or rewriting fleet defaults.
7
+ */
8
+ export declare const TOOL_PROFILES: Readonly<Record<ToolSetName, readonly string[]>>;
9
+ /**
10
+ * `Object.hasOwn`, not `in`.
11
+ *
12
+ * `"constructor" in PROFILE_SETS` is true — every object inherits it — so an
13
+ * instance whose `tool_set` was written as `constructor` or `__proto__` used to
14
+ * pass this check and then reach `PROFILE_SETS[...].has(...)` on a function,
15
+ * which throws inside the sink. A name that is not a profile has to answer no,
16
+ * whatever Object.prototype happens to carry.
17
+ */
18
+ export declare function isToolSetName(value: unknown): value is ToolSetName;
19
+ export declare function toolsFor(profile: ToolSetName): ReadonlySet<string>;
20
+ export declare function mayUseTool(profile: ToolSetName, tool: string): boolean;
21
+ /**
22
+ * Which profile an instance runs under.
23
+ *
24
+ * An explicit `tool_set` always wins, including `full`: a general that has been
25
+ * deliberately narrowed must stay narrowed, and a worker that has been
26
+ * deliberately widened must stay wide. Only when nothing was written does the
27
+ * role decide.
28
+ *
29
+ * `unsetDefault` is the answer for an ordinary instance that said nothing, and
30
+ * it is `"worker"`. It used to be every tool there is, which is what #804
31
+ * actually was: nobody chose to give a worker `create_instance`, it arrived by
32
+ * saying nothing. An instance that needs more says so.
33
+ *
34
+ * A value that is not a profile is not an explicit choice, and falls to the
35
+ * role rather than to the toolbox. Before, an unrecognised `AGEND_TOOL_SET`
36
+ * fell back to all 47 tools — a typo was the shortest path to maximum
37
+ * privilege, which is the wrong direction for a mistake to travel.
38
+ */
39
+ export declare function resolveToolSet(config: {
40
+ tool_set?: string;
41
+ general_topic?: boolean;
42
+ } | undefined, name: string, unsetDefault?: ToolSetName): ToolSetName;
43
+ /**
44
+ * The profile for a process that only has the environment variable.
45
+ *
46
+ * mcp-server runs on the other side of a spawn and never sees fleet.yaml, so it
47
+ * resolves from `AGEND_TOOL_SET` alone. Same rules, same fallback, one function
48
+ * — the alternative is two copies of "what does an unknown name mean" that
49
+ * drift the first time one of them is edited.
50
+ */
51
+ export declare function resolveToolSetFromEnv(value: string | undefined): ToolSetName;
52
+ /**
53
+ * The IPC message types that reach a handler of their own.
54
+ *
55
+ * `fleet_outbound` carries its tool name in the body; everything below is a
56
+ * type that IS a tool, dispatched at `fleet-manager`'s IPC switch without ever
57
+ * passing through the outbound handler. They were invisible to the permission
58
+ * question until this table existed, which is how `update_decision` — a tool on
59
+ * the coordinator-only list — kept a way through.
60
+ */
61
+ /**
62
+ * The tool an IPC message type means, or null if it is not one.
63
+ *
64
+ * Own-property only, for the same reason as `isToolSetName`: a message with
65
+ * `type: "constructor"` would otherwise pass the gate and hand a function to
66
+ * the permission check.
67
+ */
68
+ export declare function toolForIpcType(type: unknown): string | null;
69
+ export declare const IPC_TYPE_TOOLS: Readonly<Record<string, string>>;
70
+ /**
71
+ * Agent-endpoint ops that never reach its `OP_MAP`.
72
+ *
73
+ * `dispatchAgentOperation` answers these before the map is consulted, so a
74
+ * table built from the map alone would have six gaps in it.
75
+ */
76
+ export declare const EARLY_AGENT_OP_TOOLS: Readonly<Record<string, string>>;
77
+ /**
78
+ * What to say to an agent that was refused.
79
+ *
80
+ * It will read this as an instruction, so it says what to do instead rather
81
+ * than only what went wrong. Naming the profile matters too: "not allowed" with
82
+ * no reason invites retrying.
83
+ */
84
+ export declare function toolRefusedMessage(profile: ToolSetName, tool: string): string;
85
+ /** Where a permission question came from. Recorded so the gaps stay visible. */
86
+ export type ToolSink = "ipc-outbound" | "ipc-typed" | "agent-endpoint";
87
+ export interface ToolUseRecord {
88
+ readonly sink: ToolSink;
89
+ readonly instance: string;
90
+ readonly profile: ToolSetName;
91
+ readonly tool: string;
92
+ readonly allowed: boolean;
93
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * One answer to "may this instance use this tool", for every way of asking.
3
+ *
4
+ * There are four ways an agent can reach a fleet tool and only one of them ever
5
+ * consulted a tool list:
6
+ *
7
+ * 1. MCP `tools/list` — the model sees a filtered menu.
8
+ * 2. MCP `tools/call` — takes the name it was given and forwards it. The filter
9
+ * from (1) is never consulted, so a guessed name runs.
10
+ * 3. Writing `channel.sock` directly. It is a 0600 socket in the instance
11
+ * directory and the backend CLI runs as the same user with a shell, so an
12
+ * agent that can open a file can send `fleet_outbound` without mcp-server
13
+ * existing at all.
14
+ * 4. `POST /agent` with `agent.token`, which every instance gets on every spawn
15
+ * — not only the ones in cli mode.
16
+ *
17
+ * Which is why keeping a tool out of the schema is a way to spend fewer tokens
18
+ * and get a clearer error, and is not a control. The control is this module,
19
+ * called at the points where those four paths converge: the fleet's IPC
20
+ * dispatch and the agent endpoint. See
21
+ * `docs/design/coordinator-tool-perms.zh-TW.md`.
22
+ *
23
+ * Stage 1 wires the callers and records what they would have decided. Nothing
24
+ * here denies anything yet.
25
+ */
26
+ import { TOOLS } from "./channel/mcp-tools.js";
27
+ /** Everything an agent that is doing the work needs, and nothing that runs the fleet. */
28
+ const WORKER = [
29
+ // Talking to people.
30
+ "reply", "react", "edit_message", "download_attachment",
31
+ // Talking to peers. `report_result` is why `standard` was never usable as a
32
+ // worker profile: without it the delegate → work → report protocol has no
33
+ // last step. `delegate_task` is here because it creates nothing and needs a
34
+ // target that already exists — it is `send_to_instance` with a correlation
35
+ // id, and a month of real traffic showed 572 of its 574 calls coming from
36
+ // instances this would otherwise have silenced.
37
+ "send_to_instance", "report_result", "request_information", "broadcast", "delegate_task",
38
+ // Knowing where it is and who is next to it. All read-only.
39
+ "list_instances", "describe_instance", "list_teams", "list_models",
40
+ "get_fleet_status", "get_fleet_config", "get_usage", "get_effort", "get_instance_logs",
41
+ "list_decisions", "list_schedules", "list_deployments", "validate_config",
42
+ // Its own things.
43
+ "task", "post_decision", "set_display_name", "set_description",
44
+ // The repo it works in: a lease on the work itself, not a way to run the fleet.
45
+ "checkout_repo", "release_repo",
46
+ ];
47
+ /**
48
+ * The verbs that run the fleet rather than do the work.
49
+ *
50
+ * Every one of them either creates, destroys, silences or reconfigures
51
+ * something that belongs to somebody else.
52
+ */
53
+ const ORCHESTRATION = [
54
+ "create_instance", "delete_instance", "replace_instance",
55
+ "start_instance", "stop_instance", "pause_instance", "wake_instance", "restart_instance",
56
+ "deploy_template", "teardown_deployment",
57
+ "create_team", "delete_team", "update_team",
58
+ "update_fleet_defaults", "update_instance_config",
59
+ // Changing a decision somebody else recorded; `post_decision` adds, and stays.
60
+ "update_decision",
61
+ // An agent that can schedule things can make something that wakes itself up.
62
+ "create_schedule", "update_schedule", "delete_schedule",
63
+ ];
64
+ const ALL_TOOLS = TOOLS.map(t => t.name);
65
+ /**
66
+ * Three tiers plus the three that already existed.
67
+ *
68
+ * `general` is not `coordinator` plus channel I/O — it is deliberately smaller:
69
+ * a dispatcher has no business deleting instances or rewriting fleet defaults.
70
+ */
71
+ export const TOOL_PROFILES = {
72
+ full: ALL_TOOLS,
73
+ worker: WORKER,
74
+ coordinator: [...WORKER, ...ORCHESTRATION],
75
+ general: [
76
+ "reply", "react", "edit_message", "download_attachment",
77
+ "list_teams", "list_instances", "describe_instance", "get_fleet_status", "get_usage", "get_effort", "list_models",
78
+ "send_to_instance", "delegate_task", "request_information", "report_result", "broadcast",
79
+ "create_instance", "start_instance", "restart_instance", "wake_instance",
80
+ "task", "list_decisions", "post_decision",
81
+ "create_schedule", "list_schedules", "delete_schedule",
82
+ ],
83
+ standard: [
84
+ "reply", "react", "edit_message",
85
+ "send_to_instance", "broadcast", "list_instances", "describe_instance",
86
+ "list_decisions", "post_decision", "task", "set_display_name", "set_description",
87
+ "validate_config", "get_fleet_status", "get_usage", "get_effort", "get_instance_logs", "get_fleet_config",
88
+ ],
89
+ minimal: ["reply", "send_to_instance", "list_decisions", "download_attachment"],
90
+ };
91
+ const PROFILE_SETS = Object.fromEntries(Object.entries(TOOL_PROFILES).map(([name, tools]) => [name, new Set(tools)]));
92
+ /**
93
+ * `Object.hasOwn`, not `in`.
94
+ *
95
+ * `"constructor" in PROFILE_SETS` is true — every object inherits it — so an
96
+ * instance whose `tool_set` was written as `constructor` or `__proto__` used to
97
+ * pass this check and then reach `PROFILE_SETS[...].has(...)` on a function,
98
+ * which throws inside the sink. A name that is not a profile has to answer no,
99
+ * whatever Object.prototype happens to carry.
100
+ */
101
+ export function isToolSetName(value) {
102
+ return typeof value === "string" && Object.hasOwn(PROFILE_SETS, value);
103
+ }
104
+ export function toolsFor(profile) {
105
+ return PROFILE_SETS[profile];
106
+ }
107
+ export function mayUseTool(profile, tool) {
108
+ return PROFILE_SETS[profile].has(tool);
109
+ }
110
+ /**
111
+ * Which profile an instance runs under.
112
+ *
113
+ * An explicit `tool_set` always wins, including `full`: a general that has been
114
+ * deliberately narrowed must stay narrowed, and a worker that has been
115
+ * deliberately widened must stay wide. Only when nothing was written does the
116
+ * role decide.
117
+ *
118
+ * `unsetDefault` is the answer for an ordinary instance that said nothing, and
119
+ * it is `"worker"`. It used to be every tool there is, which is what #804
120
+ * actually was: nobody chose to give a worker `create_instance`, it arrived by
121
+ * saying nothing. An instance that needs more says so.
122
+ *
123
+ * A value that is not a profile is not an explicit choice, and falls to the
124
+ * role rather than to the toolbox. Before, an unrecognised `AGEND_TOOL_SET`
125
+ * fell back to all 47 tools — a typo was the shortest path to maximum
126
+ * privilege, which is the wrong direction for a mistake to travel.
127
+ */
128
+ export function resolveToolSet(config, name, unsetDefault = "worker") {
129
+ const explicit = config?.tool_set;
130
+ if (isToolSetName(explicit))
131
+ return explicit;
132
+ if (config?.general_topic === true || name === "general")
133
+ return "general";
134
+ return unsetDefault;
135
+ }
136
+ /**
137
+ * The profile for a process that only has the environment variable.
138
+ *
139
+ * mcp-server runs on the other side of a spawn and never sees fleet.yaml, so it
140
+ * resolves from `AGEND_TOOL_SET` alone. Same rules, same fallback, one function
141
+ * — the alternative is two copies of "what does an unknown name mean" that
142
+ * drift the first time one of them is edited.
143
+ */
144
+ export function resolveToolSetFromEnv(value) {
145
+ if (!value)
146
+ return "full";
147
+ if (isToolSetName(value))
148
+ return value;
149
+ return "worker";
150
+ }
151
+ /**
152
+ * The IPC message types that reach a handler of their own.
153
+ *
154
+ * `fleet_outbound` carries its tool name in the body; everything below is a
155
+ * type that IS a tool, dispatched at `fleet-manager`'s IPC switch without ever
156
+ * passing through the outbound handler. They were invisible to the permission
157
+ * question until this table existed, which is how `update_decision` — a tool on
158
+ * the coordinator-only list — kept a way through.
159
+ */
160
+ /**
161
+ * The tool an IPC message type means, or null if it is not one.
162
+ *
163
+ * Own-property only, for the same reason as `isToolSetName`: a message with
164
+ * `type: "constructor"` would otherwise pass the gate and hand a function to
165
+ * the permission check.
166
+ */
167
+ export function toolForIpcType(type) {
168
+ return typeof type === "string" && Object.hasOwn(IPC_TYPE_TOOLS, type) ? IPC_TYPE_TOOLS[type] : null;
169
+ }
170
+ export const IPC_TYPE_TOOLS = {
171
+ fleet_schedule_create: "create_schedule",
172
+ fleet_schedule_list: "list_schedules",
173
+ fleet_schedule_update: "update_schedule",
174
+ fleet_schedule_delete: "delete_schedule",
175
+ fleet_decision_create: "post_decision",
176
+ fleet_decision_list: "list_decisions",
177
+ fleet_decision_update: "update_decision",
178
+ fleet_task: "task",
179
+ fleet_set_display_name: "set_display_name",
180
+ fleet_set_description: "set_description",
181
+ };
182
+ /**
183
+ * Agent-endpoint ops that never reach its `OP_MAP`.
184
+ *
185
+ * `dispatchAgentOperation` answers these before the map is consulted, so a
186
+ * table built from the map alone would have six gaps in it.
187
+ */
188
+ export const EARLY_AGENT_OP_TOOLS = {
189
+ "schedule-create": "create_schedule",
190
+ "schedule-list": "list_schedules",
191
+ "schedule-update": "update_schedule",
192
+ "schedule-delete": "delete_schedule",
193
+ // `decision-post` is what agent-cli sends; the endpoint only looks at the
194
+ // prefix, so both spellings reach the same handler and both need a name.
195
+ "decision-post": "post_decision",
196
+ "decision-create": "post_decision",
197
+ "decision-list": "list_decisions",
198
+ "decision-update": "update_decision",
199
+ task: "task",
200
+ usage: "get_usage",
201
+ rename: "set_display_name",
202
+ "set-description": "set_description",
203
+ };
204
+ /**
205
+ * What to say to an agent that was refused.
206
+ *
207
+ * It will read this as an instruction, so it says what to do instead rather
208
+ * than only what went wrong. Naming the profile matters too: "not allowed" with
209
+ * no reason invites retrying.
210
+ */
211
+ export function toolRefusedMessage(profile, tool) {
212
+ return `${tool} is not available to this instance: it runs with the "${profile}" tool set, and ${tool} belongs to a coordinator. `
213
+ + "Report what you need with report_result and let the coordinator do it, "
214
+ + 'or ask an administrator to set `tool_set: coordinator` for this instance.';
215
+ }
216
+ //# sourceMappingURL=tool-permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-permissions.js","sourceRoot":"","sources":["../src/tool-permissions.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.1.6-beta.3",
3
+ "version": "2.1.6-beta.4",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {