pilotswarm-sdk 0.5.30 → 0.5.32

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 (81) hide show
  1. package/api/index.d.ts +40 -0
  2. package/api/index.js +8 -0
  3. package/api/src/session-authz.d.ts +76 -0
  4. package/api/src/session-authz.js +240 -0
  5. package/dist/agent-fqn.d.ts +72 -0
  6. package/dist/agent-fqn.d.ts.map +1 -0
  7. package/dist/agent-fqn.js +137 -0
  8. package/dist/agent-fqn.js.map +1 -0
  9. package/dist/agent-loader.d.ts.map +1 -1
  10. package/dist/agent-loader.js +22 -6
  11. package/dist/agent-loader.js.map +1 -1
  12. package/dist/agent-manager-tools.d.ts +135 -0
  13. package/dist/agent-manager-tools.d.ts.map +1 -0
  14. package/dist/agent-manager-tools.js +813 -0
  15. package/dist/agent-manager-tools.js.map +1 -0
  16. package/dist/agent-package-diff.d.ts +97 -0
  17. package/dist/agent-package-diff.d.ts.map +1 -0
  18. package/dist/agent-package-diff.js +203 -0
  19. package/dist/agent-package-diff.js.map +1 -0
  20. package/dist/agent-package-format.d.ts +2 -0
  21. package/dist/agent-package-format.d.ts.map +1 -1
  22. package/dist/agent-package-format.js +15 -0
  23. package/dist/agent-package-format.js.map +1 -1
  24. package/dist/agent-package-import-fetch.d.ts +68 -0
  25. package/dist/agent-package-import-fetch.d.ts.map +1 -0
  26. package/dist/agent-package-import-fetch.js +156 -0
  27. package/dist/agent-package-import-fetch.js.map +1 -0
  28. package/dist/agent-package-import-policy.d.ts +133 -0
  29. package/dist/agent-package-import-policy.d.ts.map +1 -0
  30. package/dist/agent-package-import-policy.js +347 -0
  31. package/dist/agent-package-import-policy.js.map +1 -0
  32. package/dist/agent-package-installer.d.ts.map +1 -1
  33. package/dist/agent-package-installer.js +30 -4
  34. package/dist/agent-package-installer.js.map +1 -1
  35. package/dist/cms-migrations.d.ts.map +1 -1
  36. package/dist/cms-migrations.js +653 -1
  37. package/dist/cms-migrations.js.map +1 -1
  38. package/dist/cms.d.ts +103 -10
  39. package/dist/cms.d.ts.map +1 -1
  40. package/dist/cms.js +85 -10
  41. package/dist/cms.js.map +1 -1
  42. package/dist/index.d.ts +2 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/inspect-tools.d.ts +68 -8
  47. package/dist/inspect-tools.d.ts.map +1 -1
  48. package/dist/inspect-tools.js +282 -21
  49. package/dist/inspect-tools.js.map +1 -1
  50. package/dist/managed-session.d.ts.map +1 -1
  51. package/dist/managed-session.js +87 -0
  52. package/dist/managed-session.js.map +1 -1
  53. package/dist/management-client.d.ts +20 -1
  54. package/dist/management-client.d.ts.map +1 -1
  55. package/dist/management-client.js +25 -0
  56. package/dist/management-client.js.map +1 -1
  57. package/dist/session-manager.d.ts +61 -0
  58. package/dist/session-manager.d.ts.map +1 -1
  59. package/dist/session-manager.js +143 -1
  60. package/dist/session-manager.js.map +1 -1
  61. package/dist/session-proxy.d.ts +9 -1
  62. package/dist/session-proxy.d.ts.map +1 -1
  63. package/dist/session-proxy.js +345 -12
  64. package/dist/session-proxy.js.map +1 -1
  65. package/dist/types.d.ts +31 -0
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/dist/web/web-management-client.d.ts +2 -0
  69. package/dist/web/web-management-client.d.ts.map +1 -1
  70. package/dist/web/web-management-client.js +6 -0
  71. package/dist/web/web-management-client.js.map +1 -1
  72. package/dist/worker.d.ts +5 -0
  73. package/dist/worker.d.ts.map +1 -1
  74. package/dist/worker.js +28 -0
  75. package/dist/worker.js.map +1 -1
  76. package/package.json +3 -3
  77. package/plugins/mgmt/agents/pilotswarm.agent.md +2 -2
  78. package/plugins/mgmt/skills/cost-latency-analysis/SKILL.md +1 -1
  79. package/plugins/mgmt/skills/graph-debug/SKILL.md +4 -4
  80. package/plugins/mgmt/skills/orchestration-session-lifecycle/SKILL.md +1 -1
  81. package/plugins/mgmt/agents/agent-tuner.agent.md +0 -316
package/api/index.d.ts CHANGED
@@ -84,3 +84,43 @@ export declare class HttpApiTransport {
84
84
  stop(): Promise<void>;
85
85
  [method: string]: any;
86
86
  }
87
+
88
+ // ── Session-tree access predicate (src/session-authz.js) ──────────
89
+ // Pure; shared by the portal runtime and the worker's agent tools so the
90
+ // "may this principal touch this session?" answer has ONE implementation.
91
+
92
+ export interface SessionAccessSnapshot {
93
+ rootSessionId: string;
94
+ isSystem: boolean;
95
+ visibility: "private" | "shared_read" | "shared_write";
96
+ owner: { displayName?: string | null; email?: string | null; subject?: string | null } | null;
97
+ viewerIsOwner: boolean;
98
+ viewerShareAccess: "read" | "write" | null;
99
+ }
100
+
101
+ export interface SessionAccessDecision {
102
+ allowed: boolean;
103
+ /** Report NOT_FOUND rather than FORBIDDEN: an admitted caller must not be able to probe which session ids exist. */
104
+ notFound?: boolean;
105
+ reason?: string;
106
+ /** An admin reached something a plain user in the same position could not see. Audit it. */
107
+ breakGlass?: boolean;
108
+ }
109
+
110
+ export type SessionAccessClass =
111
+ | "session:read" | "session:write" | "session:manage" | "session:destroy" | "session:share";
112
+
113
+ export declare const SESSION_VISIBILITY_VALUES: readonly string[];
114
+ export declare function normalizeVisibility(value: unknown, fallback: string): string;
115
+ export declare function systemSessionsReadable(env?: Record<string, string | undefined>): boolean;
116
+ export declare function relationFor(snapshot: SessionAccessSnapshot | null, opts?: { isAdmin?: boolean }): "owner" | "admin" | "collaborator";
117
+ export declare function evaluateSessionAccess(
118
+ accessClass: SessionAccessClass,
119
+ snapshot: SessionAccessSnapshot | null,
120
+ opts?: { isAdmin?: boolean; systemReadable?: boolean },
121
+ ): SessionAccessDecision;
122
+ /** Archive reads are owner-or-admin ONLY — never a share. See proposal §15 A3. */
123
+ export declare function evaluateArchiveAccess(
124
+ snapshot: SessionAccessSnapshot | null,
125
+ opts?: { isAdmin?: boolean },
126
+ ): SessionAccessDecision;
package/api/index.js CHANGED
@@ -12,5 +12,13 @@ export {
12
12
  artifactDownloadPath,
13
13
  ApiError,
14
14
  } from "./src/protocol.js";
15
+ export {
16
+ SESSION_VISIBILITY_VALUES,
17
+ normalizeVisibility,
18
+ systemSessionsReadable,
19
+ relationFor,
20
+ evaluateSessionAccess,
21
+ evaluateArchiveAccess,
22
+ } from "./src/session-authz.js";
15
23
  export { ApiClient } from "./src/api-client.js";
16
24
  export { HttpApiTransport } from "./src/http-api-transport.js";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Types for the shared session-tree access predicate.
3
+ *
4
+ * The implementation is plain ESM JS (it is imported by the portal, which is
5
+ * not TypeScript), so the worker needs this to consume it without `any`.
6
+ * Typing matters more than usual here: `allowed` and `notFound` mean opposite
7
+ * things to a caller, and an untyped decision object is one typo away from a
8
+ * gate that always passes.
9
+ */
10
+
11
+ export interface SessionAccessSnapshot {
12
+ rootSessionId?: string;
13
+ isSystem: boolean;
14
+ visibility: "private" | "shared_read" | "shared_write";
15
+ owner: { displayName?: string | null; email?: string | null; subject?: string | null } | null;
16
+ viewerIsOwner: boolean;
17
+ viewerShareAccess: "read" | "write" | null;
18
+ }
19
+
20
+ export interface SessionAccessDecision {
21
+ allowed: boolean;
22
+ /** Report NOT_FOUND rather than FORBIDDEN — an admitted caller must not be able to probe which session ids exist. */
23
+ notFound?: boolean;
24
+ reason?: string;
25
+ /** An admin reached something a plain user in the same position could not see. Audit it. */
26
+ breakGlass?: boolean;
27
+ }
28
+
29
+ export type SessionAccessClass =
30
+ | "session:read"
31
+ | "session:write"
32
+ | "session:manage"
33
+ | "session:destroy"
34
+ | "session:share";
35
+
36
+ export declare const SESSION_VISIBILITY_VALUES: readonly string[];
37
+
38
+ export declare function normalizeVisibility(value: unknown, fallback: string): string;
39
+
40
+ /** Whether ordinary users may READ system sessions (SESSIONS_SYSTEM_VISIBILITY; default true). */
41
+ export declare function systemSessionsReadable(env?: Record<string, string | undefined>): boolean;
42
+
43
+ export declare function relationFor(
44
+ snapshot: SessionAccessSnapshot | null,
45
+ opts?: { isAdmin?: boolean },
46
+ ): "owner" | "admin" | "collaborator";
47
+
48
+ export declare function evaluateSessionAccess(
49
+ accessClass: SessionAccessClass,
50
+ snapshot: SessionAccessSnapshot | null,
51
+ opts?: { isAdmin?: boolean; systemReadable?: boolean },
52
+ ): SessionAccessDecision;
53
+
54
+ /** Archive reads are owner-or-admin ONLY — never a share. See proposal §15 A3. */
55
+ export declare function evaluateArchiveAccess(
56
+ snapshot: SessionAccessSnapshot | null,
57
+ opts?: { isAdmin?: boolean },
58
+ ): SessionAccessDecision;
59
+
60
+ /** A sign-in role observation read from the users table. */
61
+ export interface RoleObservation {
62
+ role?: string | null;
63
+ seenAt?: Date | null;
64
+ }
65
+
66
+ /** How old a recorded sign-in role may be and still confer privilege. */
67
+ export declare const ROLE_OBSERVATION_MAX_AGE_MS: number;
68
+
69
+ /**
70
+ * Does a recorded sign-in role still make this principal an administrator?
71
+ * Every unknown resolves to `false`.
72
+ */
73
+ export declare function evaluateRoleObservation(
74
+ observation: RoleObservation | null,
75
+ opts?: { now?: number; maxAgeMs?: number; principal?: { provider?: string; subject?: string } | null },
76
+ ): { isAdmin: boolean; reason?: string };
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Session-tree access predicate — the ONE implementation, shared by every
3
+ * surface that has to answer "may this principal touch this session?".
4
+ *
5
+ * It lived in `packages/app/web/authz.js` and served the portal alone. The
6
+ * worker now needs the same answer: agent tools that read sessions must reach
7
+ * exactly what their session's OWNER could reach, and a second implementation
8
+ * would be a second thing to get wrong. So the pure half moved here, into the
9
+ * dependency both sides already have. The portal re-exports it, unchanged.
10
+ *
11
+ * Deliberately pure: no I/O, no env, no database. Callers fetch the access
12
+ * snapshot (`catalog.getSessionAccess`) and pass it in. That is what makes it
13
+ * usable from an Express route and from inside a tool handler.
14
+ *
15
+ * @module
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} SessionAccessSnapshot
20
+ * @property {string} rootSessionId
21
+ * @property {boolean} isSystem
22
+ * @property {"private"|"shared_read"|"shared_write"} visibility
23
+ * @property {{displayName?: string|null, email?: string|null, subject?: string|null}|null} owner
24
+ * @property {boolean} viewerIsOwner
25
+ * @property {"read"|"write"|null} viewerShareAccess
26
+ */
27
+
28
+ export const SESSION_VISIBILITY_VALUES = Object.freeze(["private", "shared_read", "shared_write"]);
29
+
30
+ const VISIBILITY_SET = new Set(SESSION_VISIBILITY_VALUES);
31
+
32
+ export function normalizeVisibility(value, fallback) {
33
+ const normalized = String(value || "").trim().toLowerCase();
34
+ return VISIBILITY_SET.has(normalized) ? normalized : fallback;
35
+ }
36
+
37
+ /**
38
+ * Are system sessions readable by ordinary users on this deployment?
39
+ *
40
+ * The portal reads `SESSIONS_SYSTEM_VISIBILITY` for exactly this; the worker
41
+ * must reach the same answer or the same user would see the PilotSwarm root
42
+ * in their session list but be told it does not exist by an agent. Default
43
+ * "read" — hiding them is the opt-in.
44
+ */
45
+ export function systemSessionsReadable(env = (typeof process !== "undefined" ? process.env : {})) {
46
+ return String(env?.SESSIONS_SYSTEM_VISIBILITY || "").trim().toLowerCase() !== "admin";
47
+ }
48
+
49
+ function ownerLabel(snapshot) {
50
+ return snapshot?.owner?.displayName || snapshot?.owner?.email || snapshot?.owner?.subject || "another user";
51
+ }
52
+
53
+ /**
54
+ * The caller's relation to a session tree, recorded on message payloads and
55
+ * shown to the agent in multi-writer sessions.
56
+ */
57
+ export function relationFor(snapshot, { isAdmin } = {}) {
58
+ if (snapshot?.viewerIsOwner) return "owner";
59
+ if (isAdmin) return "admin";
60
+ return "collaborator";
61
+ }
62
+
63
+ /**
64
+ * Evaluate one session-scoped access class against an access snapshot.
65
+ *
66
+ * @param {"session:read"|"session:write"|"session:manage"|"session:destroy"|"session:share"} accessClass
67
+ * @param {SessionAccessSnapshot|null} snapshot result of getSessionAccess (null = missing/deleted)
68
+ * @param {{isAdmin?: boolean, systemReadable?: boolean}} [opts]
69
+ * @returns {{allowed: boolean, notFound?: boolean, reason?: string, breakGlass?: boolean}}
70
+ */
71
+ export function evaluateSessionAccess(accessClass, snapshot, { isAdmin = false, systemReadable = true } = {}) {
72
+ if (!snapshot) {
73
+ // Missing/deleted session: let the underlying operation produce its
74
+ // own not-found; nothing to protect.
75
+ return { allowed: true };
76
+ }
77
+
78
+ if (isAdmin) {
79
+ // Admins pass everything; flag break-glass when this would have been
80
+ // invisible to a plain user in the same position.
81
+ const wouldBeInvisible = !snapshot.viewerIsOwner
82
+ && !snapshot.isSystem
83
+ && snapshot.visibility === "private"
84
+ && !snapshot.viewerShareAccess;
85
+ return { allowed: true, breakGlass: wouldBeInvisible };
86
+ }
87
+
88
+ const isRead = accessClass === "session:read";
89
+
90
+ if (snapshot.isSystem) {
91
+ // When system sessions are hidden from users, every class 404s so a
92
+ // write attempt can't confirm the session exists (review LOW-2).
93
+ if (!systemReadable) return { allowed: false, notFound: true };
94
+ if (isRead) return { allowed: true };
95
+ return { allowed: false, reason: "System sessions are managed by administrators." };
96
+ }
97
+
98
+ const canRead = snapshot.viewerIsOwner
99
+ || snapshot.visibility === "shared_read"
100
+ || snapshot.visibility === "shared_write"
101
+ || Boolean(snapshot.viewerShareAccess);
102
+
103
+ if (isRead) {
104
+ return canRead ? { allowed: true } : { allowed: false, notFound: true };
105
+ }
106
+
107
+ // Anything beyond read on an unreadable session is also a 404 — the
108
+ // caller must not learn the session exists from the error shape.
109
+ if (!canRead) return { allowed: false, notFound: true };
110
+
111
+ if (accessClass === "session:write") {
112
+ const canWrite = snapshot.viewerIsOwner
113
+ || snapshot.visibility === "shared_write"
114
+ || snapshot.viewerShareAccess === "write";
115
+ return canWrite
116
+ ? { allowed: true }
117
+ : { allowed: false, reason: `You have read access to this session; write access is required. Ask ${ownerLabel(snapshot)} for write access.` };
118
+ }
119
+
120
+ // manage / destroy / share: owner only (admin handled above).
121
+ return snapshot.viewerIsOwner
122
+ ? { allowed: true }
123
+ : { allowed: false, reason: `Only the session owner (${ownerLabel(snapshot)}) or an admin can do this.` };
124
+ }
125
+
126
+ /**
127
+ * Archive (dehydrated session tar) access — owner or admin ONLY, never a
128
+ * share. Deliberately NOT `evaluateSessionAccess("session:read", …)`.
129
+ *
130
+ * The tar is raw session state: pre-compaction history the UI no longer
131
+ * displays, full tool-call arguments (which can carry secrets a user pasted),
132
+ * internal scratch. Sharing a session shares a *view* of a conversation; it
133
+ * was never an offer of the substrate underneath. See the Agent Manager
134
+ * proposal, §15 A3.
135
+ *
136
+ * @param {SessionAccessSnapshot|null} snapshot
137
+ * @param {{isAdmin?: boolean}} [opts]
138
+ * @returns {{allowed: boolean, notFound?: boolean, reason?: string, breakGlass?: boolean}}
139
+ */
140
+ export function evaluateArchiveAccess(snapshot, { isAdmin = false } = {}) {
141
+ if (!snapshot) return { allowed: true };
142
+ if (isAdmin) {
143
+ // Reading someone else's raw session state is exactly what the
144
+ // authz_audit table calls a break-glass read.
145
+ return { allowed: true, breakGlass: !snapshot.viewerIsOwner };
146
+ }
147
+ if (snapshot.viewerIsOwner) return { allowed: true };
148
+ if (snapshot.viewerShareAccess || snapshot.visibility !== "private") {
149
+ return {
150
+ allowed: false,
151
+ reason:
152
+ "Session archives are available to the session owner and administrators only. "
153
+ + "A share grants the conversation, not the raw session state behind it.",
154
+ };
155
+ }
156
+ return { allowed: false, notFound: true };
157
+ }
158
+
159
+ /**
160
+ * How old a recorded sign-in role may be and still confer privilege.
161
+ *
162
+ * The stored role is an OBSERVATION — the last thing the identity provider
163
+ * told the portal — so it decays. An active portal user re-confirms it every
164
+ * few minutes; a principal who has not authenticated within this window is
165
+ * treated as a plain user, which is the fail-closed direction.
166
+ *
167
+ * The failure this bounds is concrete: an admin is demoted in the identity
168
+ * provider and never opens the portal again, while a cron-driven session of
169
+ * theirs keeps firing turns. Without a ceiling that session keeps fleet reach
170
+ * forever, because nothing would ever contradict the stored `admin`.
171
+ */
172
+ export const ROLE_OBSERVATION_MAX_AGE_MS = 12 * 60 * 60 * 1000;
173
+
174
+ /**
175
+ * The only principal that may ever hold the `anonymous` role.
176
+ *
177
+ * `anonymous` is issued by `authorizePrincipal(null, policy)` — the
178
+ * auth-disabled branch — and that branch pairs it with
179
+ * `createNoAuthUnknownPrincipal()`, which is this exact identity. So the
180
+ * binding below is not a new restriction; it is the existing invariant,
181
+ * written down where it is relied upon.
182
+ */
183
+ const ANONYMOUS_PRINCIPAL = { provider: "none", subject: "unknown" };
184
+
185
+ /**
186
+ * Does a recorded sign-in role still make this principal an administrator?
187
+ *
188
+ * Separate from `evaluateSessionAccess` on purpose: that answers "may this
189
+ * viewer touch this session", this answers "is this viewer privileged at all".
190
+ * It is a pure function so the fail-closed paths can be tested without a
191
+ * database, a worker, or a clock.
192
+ *
193
+ * `anonymous` counts as admin, because an auth-disabled deployment is a
194
+ * trusted one and the portal already grants it full access there. A worker
195
+ * that disagreed would make agents quietly more restricted than the UI beside
196
+ * them.
197
+ *
198
+ * But `anonymous` is a NAME being trusted to GRANT, which is the one direction
199
+ * the proposal's own A10 rule says is never safe — a forged name can only put
200
+ * you inside a denylist, never outside one, and this is the opposite shape. So
201
+ * it is admitted only for the single principal that can legitimately carry it.
202
+ * Nothing today can write `anonymous` for a named user; this makes that a
203
+ * property of the code rather than a property of three other files staying
204
+ * the way they are.
205
+ *
206
+ * Every unknown resolves to `false`: no role, an unrecognized role, a missing
207
+ * timestamp, or an observation past the ceiling.
208
+ *
209
+ * @param {{role?: string|null, seenAt?: Date|null}|null} observation
210
+ * @param {{now?: number, maxAgeMs?: number, principal?: {provider?: string, subject?: string}|null}} [opts]
211
+ * @returns {{isAdmin: boolean, reason?: string}}
212
+ */
213
+ export function evaluateRoleObservation(observation, { now = Date.now(), maxAgeMs = ROLE_OBSERVATION_MAX_AGE_MS, principal = null } = {}) {
214
+ const role = observation?.role ?? null;
215
+ if (role !== "admin" && role !== "anonymous") {
216
+ return { isAdmin: false, reason: role ? `role is '${role}'` : "no role recorded" };
217
+ }
218
+ if (role === "anonymous"
219
+ && !(principal
220
+ && principal.provider === ANONYMOUS_PRINCIPAL.provider
221
+ && principal.subject === ANONYMOUS_PRINCIPAL.subject)) {
222
+ return {
223
+ isAdmin: false,
224
+ reason: "the anonymous role belongs to the auth-disabled principal only",
225
+ };
226
+ }
227
+ const seenAt = observation?.seenAt ?? null;
228
+ if (!(seenAt instanceof Date) || Number.isNaN(seenAt.getTime())) {
229
+ return { isAdmin: false, reason: "role has no observation timestamp" };
230
+ }
231
+ // A future timestamp (clock skew between portal and worker) is not
232
+ // suspicious enough to refuse, but it must never extend the window
233
+ // either — which comparing the elapsed age against the ceiling already
234
+ // guarantees, since a negative age is under it.
235
+ const age = now - seenAt.getTime();
236
+ if (age > maxAgeMs) {
237
+ return { isAdmin: false, reason: `role observation is stale (${Math.floor(age / 3600_000)}h old)` };
238
+ }
239
+ return { isAdmin: true };
240
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Fully-qualified agent/package names.
3
+ *
4
+ * <package> bare — own enabled copy, then shared
5
+ * __shared:<package>[:<semver>] the deployment-wide copy, explicitly
6
+ * <owner>:<package>[:<semver>] a named owner's copy
7
+ *
8
+ * ── The collision this module exists to manage ──────────────────────────
9
+ *
10
+ * `a:b` was ALREADY meaningful before namespaces: the agent resolver reads it
11
+ * as `namespace:agentName` (a plugin's `plugin.json` name). §9 wants the same
12
+ * syntax to mean `owner:package`. Both readings are legitimate and neither can
13
+ * be dropped without breaking something.
14
+ *
15
+ * So this parser never guesses. A two-segment name is reported as AMBIGUOUS —
16
+ * carrying both readings — and the caller resolves in a defined order
17
+ * (namespace first, preserving today's behaviour, then owner). Only forms that
18
+ * are structurally unmistakable are classified outright:
19
+ *
20
+ * - a `__`-prefixed first segment is a RESERVED SENTINEL, so `__shared:x`
21
+ * can only ever be an FQN;
22
+ * - three segments end in a semver, which namespaces never had.
23
+ *
24
+ * ── Why `__` is reserved ────────────────────────────────────────────────
25
+ *
26
+ * `__shared` has to be unforgeable: if a user could register the subject
27
+ * `__shared`, or publish a package under it, they would capture every
28
+ * unqualified reference that meant "the deployment's copy". Reserving the
29
+ * whole prefix (not just the one token) keeps future sentinels mintable at
30
+ * zero cost — and per §15 A10, a name may DENY but must never GRANT, which is
31
+ * exactly how this is used: `__shared` selects a scope, it never confers rights.
32
+ *
33
+ * @module
34
+ */
35
+ /** The reserved sentinel for the deployment-wide namespace. */
36
+ export declare const SHARED_SENTINEL = "__shared";
37
+ /** Any name starting with this is reserved for the platform. */
38
+ export declare const RESERVED_PREFIX = "__";
39
+ export type AgentFqnKind = "bare" | "shared" | "owner" | "ambiguous" | "invalid";
40
+ export interface AgentFqn {
41
+ kind: AgentFqnKind;
42
+ /** The package/agent name — always present except for `invalid`. */
43
+ name: string;
44
+ /** Owner segment as written, for `owner` (and the owner reading of `ambiguous`). */
45
+ ownerRef?: string;
46
+ /** Namespace reading of an ambiguous two-segment name. */
47
+ namespaceRef?: string;
48
+ /** Pinned version, when the caller named one. */
49
+ semver?: string;
50
+ /** Why the input was rejected. Present only for `invalid`. */
51
+ reason?: string;
52
+ }
53
+ /**
54
+ * Is this name reserved for the platform?
55
+ *
56
+ * Used at BOTH gates the design names: package publish, and user registration.
57
+ * Checking only one of them would leave the sentinel forgeable from the other
58
+ * side.
59
+ */
60
+ export declare function isReservedName(value: string): boolean;
61
+ export declare function isSemver(value: string): boolean;
62
+ /**
63
+ * Parse a possibly-qualified agent or package reference.
64
+ *
65
+ * Never throws: an unusable input comes back as `kind: "invalid"` with a
66
+ * reason, because these strings arrive from models and users alike and a
67
+ * parser that throws on hostile input is a denial-of-service in the turn loop.
68
+ */
69
+ export declare function parseAgentFqn(raw: string): AgentFqn;
70
+ /** Render a parsed reference back to its canonical string, for display. */
71
+ export declare function formatAgentFqn(fqn: AgentFqn): string;
72
+ //# sourceMappingURL=agent-fqn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-fqn.d.ts","sourceRoot":"","sources":["../src/agent-fqn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,+DAA+D;AAC/D,eAAO,MAAM,eAAe,aAAa,CAAC;AAE1C,gEAAgE;AAChE,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,CAAC;AAEjF,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAUD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C;AAID;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CA0DnD;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAYpD"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Fully-qualified agent/package names.
3
+ *
4
+ * <package> bare — own enabled copy, then shared
5
+ * __shared:<package>[:<semver>] the deployment-wide copy, explicitly
6
+ * <owner>:<package>[:<semver>] a named owner's copy
7
+ *
8
+ * ── The collision this module exists to manage ──────────────────────────
9
+ *
10
+ * `a:b` was ALREADY meaningful before namespaces: the agent resolver reads it
11
+ * as `namespace:agentName` (a plugin's `plugin.json` name). §9 wants the same
12
+ * syntax to mean `owner:package`. Both readings are legitimate and neither can
13
+ * be dropped without breaking something.
14
+ *
15
+ * So this parser never guesses. A two-segment name is reported as AMBIGUOUS —
16
+ * carrying both readings — and the caller resolves in a defined order
17
+ * (namespace first, preserving today's behaviour, then owner). Only forms that
18
+ * are structurally unmistakable are classified outright:
19
+ *
20
+ * - a `__`-prefixed first segment is a RESERVED SENTINEL, so `__shared:x`
21
+ * can only ever be an FQN;
22
+ * - three segments end in a semver, which namespaces never had.
23
+ *
24
+ * ── Why `__` is reserved ────────────────────────────────────────────────
25
+ *
26
+ * `__shared` has to be unforgeable: if a user could register the subject
27
+ * `__shared`, or publish a package under it, they would capture every
28
+ * unqualified reference that meant "the deployment's copy". Reserving the
29
+ * whole prefix (not just the one token) keeps future sentinels mintable at
30
+ * zero cost — and per §15 A10, a name may DENY but must never GRANT, which is
31
+ * exactly how this is used: `__shared` selects a scope, it never confers rights.
32
+ *
33
+ * @module
34
+ */
35
+ /** The reserved sentinel for the deployment-wide namespace. */
36
+ export const SHARED_SENTINEL = "__shared";
37
+ /** Any name starting with this is reserved for the platform. */
38
+ export const RESERVED_PREFIX = "__";
39
+ /**
40
+ * Is this name reserved for the platform?
41
+ *
42
+ * Used at BOTH gates the design names: package publish, and user registration.
43
+ * Checking only one of them would leave the sentinel forgeable from the other
44
+ * side.
45
+ */
46
+ export function isReservedName(value) {
47
+ return String(value ?? "").trim().toLowerCase().startsWith(RESERVED_PREFIX);
48
+ }
49
+ /**
50
+ * A semver as this system uses it: `X.Y.Z` with optional pre-release/build.
51
+ * Deliberately strict — the third segment of an FQN is only treated as a
52
+ * version when it unmistakably IS one, so `a:b:c` with a non-version tail
53
+ * stays invalid rather than silently resolving to something else.
54
+ */
55
+ const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
56
+ export function isSemver(value) {
57
+ return SEMVER_RE.test(String(value ?? "").trim());
58
+ }
59
+ const invalid = (reason) => ({ kind: "invalid", name: "", reason });
60
+ /**
61
+ * Parse a possibly-qualified agent or package reference.
62
+ *
63
+ * Never throws: an unusable input comes back as `kind: "invalid"` with a
64
+ * reason, because these strings arrive from models and users alike and a
65
+ * parser that throws on hostile input is a denial-of-service in the turn loop.
66
+ */
67
+ export function parseAgentFqn(raw) {
68
+ // Reject non-strings rather than coercing. `String(123)` would parse as
69
+ // the perfectly valid bare name "123", and `String({})` as
70
+ // "[object Object]" — both of which hide a caller bug behind a name that
71
+ // looks legitimate all the way down to the database.
72
+ if (typeof raw !== "string")
73
+ return invalid("name must be a string");
74
+ const text = raw.trim();
75
+ if (!text)
76
+ return invalid("empty name");
77
+ const parts = text.split(":");
78
+ if (parts.some((p) => p.trim() === "")) {
79
+ // Catches ":x", "x:", "a::b" — all of which would otherwise produce a
80
+ // silently empty segment and resolve to something unintended.
81
+ return invalid(`malformed qualified name "${text}": empty segment`);
82
+ }
83
+ if (parts.length > 3) {
84
+ return invalid(`malformed qualified name "${text}": too many segments`);
85
+ }
86
+ if (parts.length === 1) {
87
+ const name = parts[0];
88
+ if (isReservedName(name)) {
89
+ return invalid(`"${name}" uses the reserved "${RESERVED_PREFIX}" prefix`);
90
+ }
91
+ return { kind: "bare", name };
92
+ }
93
+ const [first, second, third] = parts;
94
+ // A reserved first segment is unambiguous by construction.
95
+ if (isReservedName(first)) {
96
+ if (first.toLowerCase() !== SHARED_SENTINEL) {
97
+ return invalid(`"${first}" is a reserved namespace and is not recognized`);
98
+ }
99
+ if (isReservedName(second)) {
100
+ return invalid(`"${second}" uses the reserved "${RESERVED_PREFIX}" prefix`);
101
+ }
102
+ if (parts.length === 3) {
103
+ if (!isSemver(third))
104
+ return invalid(`"${third}" is not a valid semver`);
105
+ return { kind: "shared", name: second, semver: third };
106
+ }
107
+ return { kind: "shared", name: second };
108
+ }
109
+ if (isReservedName(second)) {
110
+ return invalid(`"${second}" uses the reserved "${RESERVED_PREFIX}" prefix`);
111
+ }
112
+ // Three segments end in a semver — a shape namespaces never had, so this
113
+ // is unambiguously an FQN.
114
+ if (parts.length === 3) {
115
+ if (!isSemver(third))
116
+ return invalid(`"${third}" is not a valid semver`);
117
+ return { kind: "owner", name: second, ownerRef: first, semver: third };
118
+ }
119
+ // Two segments: genuinely both readings. Report both and let the resolver
120
+ // apply its documented order rather than guessing here.
121
+ return { kind: "ambiguous", name: second, ownerRef: first, namespaceRef: first };
122
+ }
123
+ /** Render a parsed reference back to its canonical string, for display. */
124
+ export function formatAgentFqn(fqn) {
125
+ switch (fqn.kind) {
126
+ case "bare":
127
+ return fqn.name;
128
+ case "shared":
129
+ return `${SHARED_SENTINEL}:${fqn.name}${fqn.semver ? `:${fqn.semver}` : ""}`;
130
+ case "owner":
131
+ case "ambiguous":
132
+ return `${fqn.ownerRef}:${fqn.name}${fqn.semver ? `:${fqn.semver}` : ""}`;
133
+ default:
134
+ return "";
135
+ }
136
+ }
137
+ //# sourceMappingURL=agent-fqn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-fqn.js","sourceRoot":"","sources":["../src/agent-fqn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;AAE1C,gEAAgE;AAChE,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAkBpC;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IACxC,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;AAChF,CAAC;AAED;;;;;GAKG;AACH,MAAM,SAAS,GAAG,0DAA0D,CAAC;AAE7E,MAAM,UAAU,QAAQ,CAAC,KAAa;IAClC,OAAO,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,OAAO,GAAG,CAAC,MAAc,EAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAEtF;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACrC,wEAAwE;IACxE,2DAA2D;IAC3D,yEAAyE;IACzE,qDAAqD;IACrD,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACxB,IAAI,CAAC,IAAI;QAAE,OAAO,OAAO,CAAC,YAAY,CAAC,CAAC;IAExC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QACrC,sEAAsE;QACtE,8DAA8D;QAC9D,OAAO,OAAO,CAAC,6BAA6B,IAAI,kBAAkB,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,OAAO,CAAC,6BAA6B,IAAI,sBAAsB,CAAC,CAAC;IAC5E,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,IAAI,IAAI,wBAAwB,eAAe,UAAU,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAClC,CAAC;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;IAErC,2DAA2D;IAC3D,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,eAAe,EAAE,CAAC;YAC1C,OAAO,OAAO,CAAC,IAAI,KAAK,iDAAiD,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;YACzB,OAAO,OAAO,CAAC,IAAI,MAAM,wBAAwB,eAAe,UAAU,CAAC,CAAC;QAChF,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,OAAO,OAAO,CAAC,IAAI,KAAK,yBAAyB,CAAC,CAAC;YACzE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAC3D,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC5C,CAAC;IAED,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC,IAAI,MAAM,wBAAwB,eAAe,UAAU,CAAC,CAAC;IAChF,CAAC;IAED,yEAAyE;IACzE,2BAA2B;IAC3B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC,IAAI,KAAK,yBAAyB,CAAC,CAAC;QACzE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC3E,CAAC;IAED,0EAA0E;IAC1E,wDAAwD;IACxD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;AACrF,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,cAAc,CAAC,GAAa;IACxC,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,MAAM;YACP,OAAO,GAAG,CAAC,IAAI,CAAC;QACpB,KAAK,QAAQ;YACT,OAAO,GAAG,eAAe,IAAI,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACjF,KAAK,OAAO,CAAC;QACb,KAAK,WAAW;YACZ,OAAO,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC9E;YACI,OAAO,EAAE,CAAC;IAClB,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loader.d.ts","sourceRoot":"","sources":["../src/agent-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAclF;AAID,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACxB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qGAAqG;IACrG,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,qHAAqH;IACrH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oIAAoI;IACpI,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,WAAW,GAAG,kBAAkB,GAAG,yBAAyB,CAAC;IAC/E;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAuJD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CA6E/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAYhD"}
1
+ {"version":3,"file":"agent-loader.d.ts","sourceRoot":"","sources":["../src/agent-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAclF;AAID,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACxB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qGAAqG;IACrG,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,qHAAqH;IACrH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oIAAoI;IACpI,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,WAAW,GAAG,kBAAkB,GAAG,yBAAyB,CAAC;IAC/E;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAyKD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CA6E/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAYhD"}
@@ -61,6 +61,15 @@ export function systemChildAgentUUID(parentSessionId, slug) {
61
61
  ].join("-");
62
62
  }
63
63
  // ─── Frontmatter Parser ─────────────────────────────────────────
64
+ /**
65
+ * Keys whose value may be a YAML block scalar (`|` literal, `>` folded).
66
+ *
67
+ * `description` belongs here because a multi-line description is the natural
68
+ * way to write one, and the failure is silent: the value lands as the literal
69
+ * "|" and the indented body is dropped on the floor. That shipped — the Agent
70
+ * Manager's description read "|" in every picker and listing.
71
+ */
72
+ const BLOCK_SCALAR_KEYS = new Set(["splash", "splashMobile", "initialPrompt", "description"]);
64
73
  /**
65
74
  * Parse YAML frontmatter from an .agent.md file.
66
75
  * Handles simple `key: value` pairs and YAML list syntax for `tools` and `skills`.
@@ -90,6 +99,11 @@ function parseAgentFrontmatter(content) {
90
99
  // For > (folded) scalars, collapse newlines to spaces
91
100
  meta.initialPrompt = currentBlockStyle === ">" ? val.replace(/\n/g, " ").trim() : val;
92
101
  }
102
+ else if (currentKey === "description") {
103
+ // A description is one paragraph wherever it is displayed, so
104
+ // both styles collapse to a single line.
105
+ meta.description = val.replace(/\n/g, " ").trim();
106
+ }
93
107
  multilineValue = null;
94
108
  currentBlockStyle = null;
95
109
  }
@@ -151,7 +165,14 @@ function parseAgentFrontmatter(content) {
151
165
  value = value.slice(1, -1);
152
166
  }
153
167
  currentKey = key;
154
- if (key === "name")
168
+ // Block scalars are detected before the per-key branches below: those
169
+ // assign `value` verbatim, so a `key: |` reaching them would store the
170
+ // literal "|" and orphan the indented lines that follow.
171
+ if (BLOCK_SCALAR_KEYS.has(key) && (value === "|" || value === ">")) {
172
+ currentBlockStyle = value;
173
+ multilineValue = [];
174
+ }
175
+ else if (key === "name")
155
176
  meta.name = value;
156
177
  else if (key === "description")
157
178
  meta.description = value;
@@ -198,11 +219,6 @@ function parseAgentFrontmatter(content) {
198
219
  else if (key === "inheritDefaultMcpServers") {
199
220
  meta.inheritDefaultMcpServers = value === "true";
200
221
  }
201
- else if ((key === "splash" || key === "splashMobile" || key === "initialPrompt") && (value === "|" || value === ">")) {
202
- // YAML block scalar (| literal, > folded)
203
- currentBlockStyle = value;
204
- multilineValue = [];
205
- }
206
222
  else if (key === "splash") {
207
223
  meta.splash = value;
208
224
  }