@nanmicoder/dsh-agent-teams 0.1.22-rc.1 → 0.1.23

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.
Files changed (46) hide show
  1. package/README.md +35 -12
  2. package/README_ZH.md +35 -12
  3. package/compatibility.json +6 -10
  4. package/lib/artifacts.js +342 -0
  5. package/lib/capabilities.js +6 -4
  6. package/lib/client/ActivityPanel.js +5 -2
  7. package/lib/client/agent-teams-card-definition.js +100 -6
  8. package/lib/client.js +90 -7
  9. package/lib/client.js.map +1 -1
  10. package/lib/command.js +3 -2
  11. package/lib/git-artifact-stamp.json +2 -2
  12. package/lib/harness-compat.js +70 -21
  13. package/lib/index.js +26 -3
  14. package/lib/lineage.js +198 -0
  15. package/lib/mailbox.js +59 -34
  16. package/lib/members.js +301 -53
  17. package/lib/profiles.js +3 -2
  18. package/lib/quality-gates.js +850 -65
  19. package/lib/scheduler.js +22 -3
  20. package/lib/snapshot.js +22 -1
  21. package/lib/state.js +279 -13
  22. package/lib/tool-policies.js +187 -0
  23. package/lib/tools.js +370 -89
  24. package/lib/types/artifacts.d.ts +66 -0
  25. package/lib/types/client/activity-monitor.d.ts +6 -0
  26. package/lib/types/client/agent-teams-card-definition.d.ts +56 -0
  27. package/lib/types/harness-compat.d.ts +56 -14
  28. package/lib/types/index.d.ts +26 -1
  29. package/lib/types/lineage.d.ts +92 -0
  30. package/lib/types/members.d.ts +112 -1
  31. package/lib/types/profiles.d.ts +6 -0
  32. package/lib/types/quality-gates.d.ts +216 -10
  33. package/lib/types/scheduler.d.ts +2 -1
  34. package/lib/types/snapshot.d.ts +20 -0
  35. package/lib/types/state.d.ts +22 -5
  36. package/lib/types/tool-policies.d.ts +119 -0
  37. package/lib/types/tools.d.ts +12 -1
  38. package/lib/types/types.d.ts +95 -0
  39. package/lib/types/workspace-audit.d.ts +27 -0
  40. package/lib/workspace-audit.js +52 -0
  41. package/package.json +59 -52
  42. package/pnpm-workspace.yaml +273 -273
  43. package/release-notes/v0.1.22.md +18 -0
  44. package/release-notes/v0.1.23.md +34 -0
  45. package/scripts/doctor.mjs +4 -4
  46. package/scripts/legacy-session-source-repair.mjs +344 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Owner-declared, reusable per-member tool policies (#59).
3
+ *
4
+ * A deployment owner declares reusable policies under `toolPolicies`; a member
5
+ * *selects* one by id (`tool_policy` on `agent_teams_add_member`, an inline
6
+ * plan member, or a profile member row). The Captain model can therefore only
7
+ * choose an existing policy — it can never submit raw allow/deny lists or
8
+ * invent one at runtime.
9
+ *
10
+ * One pure resolver turns the selection into the immutable effective filter
11
+ * that is persisted on the member (`toolPolicy` + `toolFilter`) and handed to
12
+ * the existing `startContinuable({ toolFilter })` seam, which hides the named
13
+ * tools from the child's schemas and rejects their execution. This is tool
14
+ * visibility / invocation policy for global tools, not an OS sandbox.
15
+ *
16
+ * Invariants (issue #59):
17
+ * 1. the captain-only deny list always applies and cannot be undone by a
18
+ * policy — the host intersects `allow` and `deny`, so a denied name never
19
+ * becomes visible again;
20
+ * 2. the four member coordination tools stay available, and a policy may not
21
+ * deny them;
22
+ * 3. an allow-list policy explicitly includes those coordination tools;
23
+ * 4. the effective deny list is the union of the policy deny list and the
24
+ * captain-only deny list.
25
+ * @module dsh-agent-teams/tool-policies
26
+ */
27
+ import { CAPTAIN_TOOL_NAMES, MEMBER_TOOL_NAMES } from "./tool-names.js";
28
+ /** Host tools denied when member delegation is switched off (`memberMaxDepth: 0`). */
29
+ const DEPTH_DENY_TOOLS = ['subagent', 'send_message'];
30
+ /** The PTC presentation transport is reserved and cannot be named by a filter. */
31
+ const RESERVED_TOOL_NAME = 'run_code';
32
+ /**
33
+ * Deny names the lenient spawn retry may drop when the host reports them
34
+ * unknown: exactly the built-in captain-only and depth entries (#163/#164).
35
+ * Names an owner policy declares are never relaxed — a policy that the host
36
+ * cannot apply must fail loud instead of silently applying a different one.
37
+ */
38
+ export const BUILTIN_DENY_TOOLS = [...CAPTAIN_TOOL_NAMES, ...DEPTH_DENY_TOOLS];
39
+ /** Trim, drop blanks, and de-duplicate names, preserving first-seen order. */
40
+ export function dedupeToolNames(names) {
41
+ const seen = new Set();
42
+ const result = [];
43
+ for (const raw of names) {
44
+ const name = raw.trim();
45
+ if (name === '' || seen.has(name))
46
+ continue;
47
+ seen.add(name);
48
+ result.push(name);
49
+ }
50
+ return result;
51
+ }
52
+ /**
53
+ * Validate and normalize one declared policy. Empty policies and empty lists
54
+ * are configuration mistakes: an `allow: []` policy would hide even the
55
+ * coordination tools, and a policy with neither list restricts nothing.
56
+ * Denying a required coordination tool is rejected for the same reason.
57
+ * @param id - the policy id, used in error messages.
58
+ * @param policy - the declared policy.
59
+ * @returns the normalized policy (trimmed, de-duplicated, blanks dropped).
60
+ */
61
+ export function normalizeToolPolicy(id, policy) {
62
+ const allow = policy.allow === undefined ? undefined : dedupeToolNames(policy.allow);
63
+ const deny = policy.deny === undefined ? undefined : dedupeToolNames(policy.deny);
64
+ if (allow === undefined && deny === undefined) {
65
+ throw new Error(`tool policy "${id}" must declare "allow" and/or "deny"`);
66
+ }
67
+ if (allow !== undefined && allow.length === 0) {
68
+ throw new Error(`tool policy "${id}" has an empty allow list`);
69
+ }
70
+ if (deny !== undefined && deny.length === 0) {
71
+ throw new Error(`tool policy "${id}" has an empty deny list`);
72
+ }
73
+ const reserved = [...allow ?? [], ...deny ?? []].filter(name => name === RESERVED_TOOL_NAME);
74
+ if (reserved.length > 0) {
75
+ throw new Error(`tool policy "${id}" names the reserved presentation transport "${RESERVED_TOOL_NAME}"`);
76
+ }
77
+ const required = (deny ?? []).filter(name => MEMBER_TOOL_NAMES.includes(name));
78
+ if (required.length > 0) {
79
+ throw new Error(`tool policy "${id}" denies required member coordination tool${required.length > 1 ? 's' : ''} ${required.join(', ')}; those tools stay available to every member`);
80
+ }
81
+ return {
82
+ ...allow === undefined ? {} : { allow },
83
+ ...deny === undefined ? {} : { deny },
84
+ };
85
+ }
86
+ /**
87
+ * Validate every configured policy once, at plugin mount, so a malformed
88
+ * deployment config fails loud instead of at the first member creation.
89
+ * @param raw - the configured `toolPolicies` map.
90
+ * @returns normalized policies keyed by trimmed id.
91
+ */
92
+ export function parseToolPolicies(raw) {
93
+ // Null-prototype: a policy id is data, so `constructor`, `toString`, or
94
+ // `__proto__` must never resolve through the object prototype.
95
+ const policies = Object.create(null);
96
+ for (const [rawId, policy] of Object.entries(raw ?? {})) {
97
+ const id = rawId.trim();
98
+ if (id === '')
99
+ throw new Error('configured toolPolicies include an empty policy id');
100
+ if (Object.hasOwn(policies, id))
101
+ throw new Error(`configured toolPolicies include duplicate policy id "${id}"`);
102
+ policies[id] = normalizeToolPolicy(id, policy);
103
+ }
104
+ return policies;
105
+ }
106
+ /**
107
+ * The policy behind one selected id, or `undefined` when this deployment no
108
+ * longer declares it (a config change after the member was created). Used on
109
+ * paths that must not fail a durable record merely because the owner edited
110
+ * configuration; selection itself uses {@link requireToolPolicy}.
111
+ * @param policies - normalized configured policies.
112
+ * @param id - the selected policy id.
113
+ * @returns the declared policy, or undefined.
114
+ */
115
+ export function optionalToolPolicy(policies, id) {
116
+ return Object.hasOwn(policies, id) ? policies[id] : undefined;
117
+ }
118
+ /**
119
+ * The deny names one spawn may relax for a member that selected a policy.
120
+ *
121
+ * The built-in captain/depth entries keep the #163/#164 recovery path, but a
122
+ * name the owner explicitly declared in the policy's own `deny` list is never
123
+ * relaxable — even when it is also a built-in entry (`send_message` and
124
+ * `subagent` are depth entries, and every captain-only name is a built-in
125
+ * deny). Dropping such a name would spawn the member under a different policy
126
+ * than the owner declared, which is contract drift rather than recovery.
127
+ * @param policy - the member's selected policy, or undefined when the member
128
+ * selected none (or the id is no longer configured).
129
+ * @param policySelected - whether the member selected a policy at all. A
130
+ * selected id whose policy is no longer configured relaxes nothing, so the
131
+ * member fails loud instead of spawning under an unknown filter.
132
+ * @returns the deny names the lenient retry may drop.
133
+ */
134
+ export function memberSpawnRelaxableDeny(policy, policySelected = policy !== undefined) {
135
+ if (!policySelected)
136
+ return BUILTIN_DENY_TOOLS;
137
+ if (policy === undefined)
138
+ return [];
139
+ const ownerDeny = new Set(policy.deny ?? []);
140
+ return BUILTIN_DENY_TOOLS.filter(name => !ownerDeny.has(name));
141
+ }
142
+ /**
143
+ * The policy behind one selected id. A model-facing selection may only name a
144
+ * policy the deployment owner declared, so an unknown id is an error raised
145
+ * before any member is persisted.
146
+ * @param policies - normalized configured policies.
147
+ * @param id - the selected policy id.
148
+ * @returns the declared policy.
149
+ */
150
+ export function requireToolPolicy(policies, id) {
151
+ // Own-property lookup only: `constructor`, `toString`, and `__proto__` are
152
+ // unknown ids, not accidentally inherited functions.
153
+ const policy = Object.hasOwn(policies, id) ? policies[id] : undefined;
154
+ if (policy === undefined) {
155
+ const available = Object.keys(policies).sort();
156
+ throw new Error(`unknown tool policy "${id}"${available.length === 0
157
+ ? ' (no toolPolicies are configured in this deployment)'
158
+ : ` (configured: ${available.join(', ')})`}`);
159
+ }
160
+ return policy;
161
+ }
162
+ /**
163
+ * Turn a selected policy (or no policy) into the immutable effective filter.
164
+ *
165
+ * Without `knownTools` the host's delegation names are forwarded verbatim, as
166
+ * before #163: the spawn retry drops whatever the host reports unknown.
167
+ * @param policy - the normalized policy, or undefined for the legacy behavior.
168
+ * @param context - depth cap and the host's restrictable-name view.
169
+ * @returns the effective filter handed to `startContinuable`.
170
+ */
171
+ export function resolveMemberToolFilter(policy, context = {}) {
172
+ if (policy !== undefined && policy.allow === undefined && policy.deny === undefined) {
173
+ throw new Error('a tool policy must declare "allow" and/or "deny"');
174
+ }
175
+ const depthDeny = context.maxDepth === 0
176
+ ? DEPTH_DENY_TOOLS.filter(name => context.knownTools === undefined || context.knownTools.has(name))
177
+ : [];
178
+ const deny = dedupeToolNames([...(policy?.deny ?? []), ...CAPTAIN_TOOL_NAMES, ...depthDeny]);
179
+ if (policy?.allow === undefined)
180
+ return { deny };
181
+ // A denied name can never be re-admitted: allow and deny intersect in the
182
+ // host, and the snapshot must say the same thing the host will enforce.
183
+ const allow = dedupeToolNames([...policy.allow, ...MEMBER_TOOL_NAMES]).filter(name => !deny.includes(name));
184
+ if (allow.length === 0)
185
+ throw new Error('a tool policy allow list cannot be empty once captain-only tools are removed');
186
+ return { allow, deny };
187
+ }