@huynle/hub 0.7.4 → 0.8.1

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 (241) hide show
  1. package/CHANGELOG.md +430 -1
  2. package/Dockerfile +7 -2
  3. package/README.md +230 -32
  4. package/package.json +5 -3
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/daemon.ts +8 -1
  7. package/packages/claude-code/src/launcher.ts +29 -9
  8. package/packages/claude-code/src/paths.ts +3 -3
  9. package/packages/claude-code/src/profile.ts +173 -0
  10. package/packages/claude-code/src/sessions.ts +22 -4
  11. package/packages/cli/dist/hub-cli.js +6871 -3460
  12. package/packages/cli/package.json +1 -1
  13. package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
  14. package/packages/cli/src/artifacts-mcp.ts +160 -0
  15. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
  16. package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
  17. package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
  18. package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
  19. package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
  20. package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
  21. package/packages/cli/src/cli.ts +96 -14
  22. package/packages/cli/src/format.ts +48 -10
  23. package/packages/cli/src/http.ts +2 -0
  24. package/packages/cli/src/intents.ts +23 -0
  25. package/packages/cli/src/launches.ts +9 -3
  26. package/packages/cli/src/login.ts +20 -6
  27. package/packages/cli/src/mcp.ts +385 -10
  28. package/packages/cli/src/messaging.ts +7 -3
  29. package/packages/cli/src/profiles-mcp.ts +301 -0
  30. package/packages/cli/src/setup/commands.ts +23 -4
  31. package/packages/cli/src/setup/runtime.ts +3 -2
  32. package/packages/cli/src/spoke-services.ts +1 -1
  33. package/packages/cli/src/team-intake-mcp.ts +158 -0
  34. package/packages/cli/src/teams-mcp.ts +460 -0
  35. package/packages/cli/src/watchlist.ts +135 -0
  36. package/packages/codex/package.json +1 -1
  37. package/packages/codex/src/paths.ts +3 -3
  38. package/packages/codex/src/profile.ts +163 -0
  39. package/packages/codex/src/sessions.ts +12 -0
  40. package/packages/codex/src/spoke.ts +19 -1
  41. package/packages/node/package.json +1 -1
  42. package/packages/node/src/config.ts +104 -2
  43. package/packages/node/src/opencode-warm.ts +344 -0
  44. package/packages/node/src/run.ts +28 -1
  45. package/packages/preset/package.json +1 -1
  46. package/packages/preset/src/profile.ts +34 -0
  47. package/packages/preset/src/spoke.ts +4 -0
  48. package/packages/protocol/package.json +1 -1
  49. package/packages/protocol/src/artifacts.ts +142 -0
  50. package/packages/protocol/src/concierge.ts +366 -0
  51. package/packages/protocol/src/contracts.ts +104 -0
  52. package/packages/protocol/src/graph.ts +70 -0
  53. package/packages/protocol/src/hub-workspace.ts +201 -0
  54. package/packages/protocol/src/index.ts +175 -6
  55. package/packages/protocol/src/launch.ts +36 -0
  56. package/packages/protocol/src/profiles.ts +301 -0
  57. package/packages/protocol/src/teams.ts +204 -0
  58. package/packages/runtime-openshell/package.json +1 -1
  59. package/packages/runtime-sdk/package.json +1 -1
  60. package/packages/sdk/package.json +1 -1
  61. package/packages/sdk/src/address.ts +50 -2
  62. package/packages/sdk/src/artifact-jobs.ts +194 -0
  63. package/packages/sdk/src/artifacts.ts +187 -0
  64. package/packages/sdk/src/client.ts +226 -5
  65. package/packages/sdk/src/concierge.ts +88 -0
  66. package/packages/sdk/src/errors.ts +4 -1
  67. package/packages/sdk/src/index.ts +6 -0
  68. package/packages/sdk/src/team-intake.ts +122 -0
  69. package/packages/sdk/src/teams.ts +346 -0
  70. package/packages/sdk/src/types.ts +568 -1
  71. package/packages/sdk/src/wake.ts +20 -0
  72. package/packages/spoke-sdk/package.json +1 -1
  73. package/packages/spoke-sdk/src/index.ts +1 -0
  74. package/packages/spoke-sdk/src/profile-apply.ts +245 -0
  75. package/src/central/actions.ts +7 -0
  76. package/src/central/agent/acting-routes.ts +2 -1
  77. package/src/central/agent/acting-switch.ts +2 -1
  78. package/src/central/agent/acting.ts +218 -10
  79. package/src/central/agent/app-tokens.ts +3 -2
  80. package/src/central/agent/audit.ts +18 -1
  81. package/src/central/agent/body.ts +163 -0
  82. package/src/central/agent/decisions.ts +125 -2
  83. package/src/central/agent/delivery.ts +194 -40
  84. package/src/central/agent/directory-routes.ts +37 -7
  85. package/src/central/agent/directory.ts +70 -14
  86. package/src/central/agent/errors.ts +7 -0
  87. package/src/central/agent/escalation.ts +10 -2
  88. package/src/central/agent/events-routes.ts +21 -4
  89. package/src/central/agent/events.ts +18 -4
  90. package/src/central/agent/form-check.ts +85 -0
  91. package/src/central/agent/grant-requests.ts +2 -1
  92. package/src/central/agent/grant-routes.ts +4 -3
  93. package/src/central/agent/grants.ts +29 -2
  94. package/src/central/agent/idempotency.ts +133 -0
  95. package/src/central/agent/intent-routes.ts +116 -0
  96. package/src/central/agent/intents.ts +505 -0
  97. package/src/central/agent/interaction-graph-routes.ts +212 -0
  98. package/src/central/agent/interaction-graph.ts +865 -0
  99. package/src/central/agent/launch-engine.ts +189 -32
  100. package/src/central/agent/launch-routes.ts +26 -10
  101. package/src/central/agent/launch-spec.ts +40 -3
  102. package/src/central/agent/launch-store.ts +55 -2
  103. package/src/central/agent/message-limits.ts +89 -11
  104. package/src/central/agent/message-routes.ts +10 -4
  105. package/src/central/agent/messages.ts +28 -2
  106. package/src/central/agent/names.ts +12 -2
  107. package/src/central/agent/placement.ts +93 -18
  108. package/src/central/agent/read-routes.ts +89 -19
  109. package/src/central/agent/routes.ts +178 -5
  110. package/src/central/agent/webhook-routes.ts +2 -1
  111. package/src/central/agent/webhooks.ts +17 -2
  112. package/src/central/agent-data.ts +4 -0
  113. package/src/central/artifacts/access.ts +44 -0
  114. package/src/central/artifacts/blob-store.ts +113 -0
  115. package/src/central/artifacts/blob-support.ts +98 -0
  116. package/src/central/artifacts/events.ts +27 -0
  117. package/src/central/artifacts/host-caps.ts +162 -0
  118. package/src/central/artifacts/host.ts +315 -0
  119. package/src/central/artifacts/idempotency.ts +109 -0
  120. package/src/central/artifacts/jobs-routes.ts +776 -0
  121. package/src/central/artifacts/jobs.ts +344 -0
  122. package/src/central/artifacts/maintenance.ts +103 -0
  123. package/src/central/artifacts/routes.ts +360 -0
  124. package/src/central/artifacts/store.ts +776 -0
  125. package/src/central/artifacts/workspaces.ts +31 -0
  126. package/src/central/backend/file-blobs.ts +377 -0
  127. package/src/central/backend/index.ts +12 -3
  128. package/src/central/backend/memory-blobs.ts +116 -0
  129. package/src/central/backend/postgres-blobs.ts +513 -0
  130. package/src/central/backend/postgres-migrate.ts +33 -0
  131. package/src/central/backend/postgres-state.ts +19 -0
  132. package/src/central/backend/types.ts +8 -0
  133. package/src/central/concierge/activity.ts +142 -0
  134. package/src/central/concierge/arm.ts +63 -0
  135. package/src/central/concierge/board-render.ts +190 -0
  136. package/src/central/concierge/board-results.ts +169 -0
  137. package/src/central/concierge/board-routes.ts +350 -0
  138. package/src/central/concierge/board.ts +249 -0
  139. package/src/central/concierge/context.ts +221 -0
  140. package/src/central/concierge/entitlements.ts +13 -0
  141. package/src/central/concierge/guard.ts +120 -0
  142. package/src/central/concierge/inbound.ts +86 -0
  143. package/src/central/concierge/index.ts +84 -0
  144. package/src/central/concierge/notes.ts +41 -0
  145. package/src/central/concierge/notifier.ts +986 -0
  146. package/src/central/concierge/prompt.ts +131 -0
  147. package/src/central/concierge/proposals.ts +268 -0
  148. package/src/central/concierge/push-remap.ts +49 -0
  149. package/src/central/concierge/routes.ts +122 -0
  150. package/src/central/concierge/schedule.ts +113 -0
  151. package/src/central/concierge/service.ts +243 -0
  152. package/src/central/concierge/session-routes.ts +125 -0
  153. package/src/central/concierge/settings.ts +217 -0
  154. package/src/central/concierge/supervisor.ts +767 -0
  155. package/src/central/concierge/tiers.ts +260 -0
  156. package/src/central/concierge/watchlist-routes.ts +279 -0
  157. package/src/central/concierge/watchlist.ts +273 -0
  158. package/src/central/config.ts +28 -6
  159. package/src/central/http.ts +660 -54
  160. package/src/central/main.ts +1 -1
  161. package/src/central/mcp-http.ts +10 -21
  162. package/src/central/profiles/apply.ts +333 -0
  163. package/src/central/profiles/content.ts +174 -0
  164. package/src/central/profiles/events.ts +40 -0
  165. package/src/central/profiles/idempotency.ts +14 -0
  166. package/src/central/profiles/import.ts +139 -0
  167. package/src/central/profiles/routes.ts +356 -0
  168. package/src/central/profiles/store.ts +597 -0
  169. package/src/central/profiles/usage-ports.ts +120 -0
  170. package/src/central/profiles/usage.ts +364 -0
  171. package/src/central/push.ts +7 -0
  172. package/src/central/registry.ts +69 -7
  173. package/src/central/store.ts +4 -0
  174. package/src/central/teams/collections.ts +34 -0
  175. package/src/central/teams/discovery.ts +346 -0
  176. package/src/central/teams/draft-edits.ts +46 -0
  177. package/src/central/teams/events.ts +46 -0
  178. package/src/central/teams/intake-routes.ts +100 -0
  179. package/src/central/teams/intake.ts +786 -0
  180. package/src/central/teams/policy.ts +231 -0
  181. package/src/central/teams/routes.ts +516 -0
  182. package/src/central/teams/seats.ts +272 -0
  183. package/src/central/teams/staffing.ts +910 -0
  184. package/src/central/teams/store.ts +667 -0
  185. package/src/central/teams/validation.ts +256 -0
  186. package/src/central/workspaces/board.ts +408 -0
  187. package/src/central/workspaces/checkpoints.ts +316 -0
  188. package/src/central/workspaces/coordinator.ts +841 -0
  189. package/src/central/workspaces/events.ts +85 -0
  190. package/src/central/workspaces/lifecycle.ts +93 -0
  191. package/src/central/workspaces/master.ts +490 -0
  192. package/src/central/workspaces/members.ts +632 -0
  193. package/src/central/workspaces/routes.ts +354 -0
  194. package/src/central/workspaces/sidechats-routes.ts +410 -0
  195. package/src/central/workspaces/sidechats.ts +482 -0
  196. package/src/central/workspaces/store.ts +571 -0
  197. package/src/central/workspaces/supervision.ts +355 -0
  198. package/src/index.ts +126 -23
  199. package/src/migrate.ts +109 -3
  200. package/src/spoke/concierge-agent.ts +33 -0
  201. package/src/spoke/deliver.ts +22 -4
  202. package/src/spoke/env-inject.ts +8 -1
  203. package/src/spoke/fork-revert.ts +142 -0
  204. package/src/spoke/hub-rpc.ts +41 -5
  205. package/src/spoke/hub-tool.ts +404 -25
  206. package/src/spoke/launchers.ts +6 -4
  207. package/src/spoke/listing.ts +65 -10
  208. package/src/spoke/localhub.ts +14 -4
  209. package/src/spoke/locations.ts +40 -0
  210. package/src/spoke/profile-apply.ts +298 -0
  211. package/src/spoke/session-context.ts +256 -0
  212. package/src/spoke/spoke.ts +330 -23
  213. package/src/spoke/timeline.ts +6 -1
  214. package/src/spoke/tracker.ts +23 -1
  215. package/src/spoke/who-teams.ts +81 -0
  216. package/src/tui-messaging.ts +32 -18
  217. package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
  218. package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
  219. package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
  220. package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
  221. package/web/dist/assets/Library-DYsFj_5c.css +1 -0
  222. package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
  223. package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
  224. package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
  225. package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
  226. package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
  227. package/web/dist/assets/index-BabrNDB3.css +1 -0
  228. package/web/dist/assets/index-lIOfSYck.js +34 -0
  229. package/web/dist/assets/live-BDq0AABg.js +5 -0
  230. package/web/dist/assets/live-BUofsQpz.css +1 -0
  231. package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
  232. package/web/dist/assets/plural-Bspvoqfq.js +1 -0
  233. package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
  234. package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
  235. package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
  236. package/web/dist/assets/source-CDU04kYw.js +1 -0
  237. package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
  238. package/web/dist/index.html +5 -2
  239. package/web/dist/sw.js +22 -2
  240. package/web/dist/assets/index-Dm8OmkaO.css +0 -1
  241. package/web/dist/assets/index-FUJbj_Du.js +0 -20
@@ -0,0 +1,245 @@
1
+ // Profile application for harness adapters (HWA-15; contract packages/protocol/src/profiles.ts). Shared by the
2
+ // OpenCode (HWA-16), Claude Code (HWA-17) and Codex/preset (HWA-18) adapters; checked by the conformance suite.
3
+ //
4
+ // Precedence (the documented rule): a profile ADDS a session-scoped layer and never replaces local configuration.
5
+ // - Local instructions (project AGENTS.md/CLAUDE.md, the user's own harness config) load unchanged. Profile
6
+ // instructions come after them, framed by profileInstructionsBlock, which says local instructions take
7
+ // precedence where they conflict.
8
+ // - A local skill or command with the same name wins: the profile's is skipped with a note (status partial).
9
+ // - Parts the adapter cannot apply are skipped with a note (partial); nothing applicable is `unsupported`.
10
+ // - An adapter whose harness reads the profile only at session start answers `restart_required`.
11
+ // - Adapters write session-scoped state only, never global harness configuration.
12
+ // Secrets: profiles carry `{{secret:NAME}}` references only (central never resolves them). The adapter resolves
13
+ // them on the spoke's machine when a command is rendered at invocation (renderProfileCommand; default source
14
+ // envSecrets: HUB_SECRET_<NAME> in the spoke's environment). Resolved values are never written to disk, sent to
15
+ // the hub, or put in notes; an adapter that cannot render at invocation skips commands that reference secrets.
16
+ import { createHash } from "node:crypto"
17
+ import {
18
+ canonicalProfileJSON,
19
+ PROFILE_APPLY_LIMITS,
20
+ type ProfileApplyRequest,
21
+ type ProfileApplyResult,
22
+ type ProfileApplyStatus,
23
+ type ProfileCommand,
24
+ type ProfileSkill,
25
+ profileTemplateParts,
26
+ type ProfileVersionContent,
27
+ } from "../../protocol/src/index.ts"
28
+
29
+ /** sha256 hex of the canonical JSON of a version's content: what the hub sends as `digest`. */
30
+ export function profileDigest(content: ProfileVersionContent): string {
31
+ return createHash("sha256").update(canonicalProfileJSON(content)).digest("hex")
32
+ }
33
+
34
+ /** What a harness adapter can apply to a running session. */
35
+ export interface ProfileCapabilities {
36
+ readonly instructions: boolean
37
+ readonly skills: boolean
38
+ readonly commands: boolean
39
+ /** It can switch the session's model. */
40
+ readonly model: boolean
41
+ /** Applied parts take effect in the running session; false: only after the session restarts. */
42
+ readonly live: boolean
43
+ /** It renders commands when they are invoked (so secret references resolve then, on this machine). */
44
+ readonly renderAtInvocation: boolean
45
+ }
46
+
47
+ /** Names the session already has locally (they win over the profile's). */
48
+ export interface ProfileLocalNames {
49
+ readonly skills?: Iterable<string>
50
+ readonly commands?: Iterable<string>
51
+ }
52
+
53
+ export interface ProfilePlan {
54
+ readonly status: ProfileApplyStatus
55
+ /** The framed instructions block to add after local instructions (absent: none to apply). */
56
+ readonly instructions?: string
57
+ readonly skills: readonly ProfileSkill[]
58
+ readonly commands: readonly ProfileCommand[]
59
+ readonly model?: string
60
+ readonly notes: readonly string[]
61
+ }
62
+
63
+ /** A note within the protocol's bounds: plain text of at most noteChars characters. */
64
+ export function profileNote(text: string): string {
65
+ const clean = text.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, " ")
66
+ return clean.length <= PROFILE_APPLY_LIMITS.noteChars ? clean : `${clean.slice(0, PROFILE_APPLY_LIMITS.noteChars - 1)}…`
67
+ }
68
+
69
+ /** At most PROFILE_APPLY_LIMITS.notes notes (the last one says how many were left out). */
70
+ export function boundedNotes(notes: readonly string[]): string[] {
71
+ const max = PROFILE_APPLY_LIMITS.notes
72
+ const clean = notes.map(profileNote)
73
+ return clean.length <= max ? clean : [...clean.slice(0, max - 1), `… ${clean.length - max + 1} more notes`]
74
+ }
75
+
76
+ /** The profile instructions as adapters add them: after local instructions, which take precedence. */
77
+ export function profileInstructionsBlock(req: Pick<ProfileApplyRequest, "profileID" | "version" | "content">): string {
78
+ return [
79
+ `<!-- hub profile ${req.profileID} v${req.version} (session-scoped) -->`,
80
+ `Hub agent profile ${req.profileID} version ${req.version}, applied to this session only. Where it conflicts with local project or user instructions, the local instructions take precedence.`,
81
+ "",
82
+ req.content.instructions ?? "",
83
+ ].join("\n")
84
+ }
85
+
86
+ /** Secret names a template references. */
87
+ export function templateSecrets(template: string): string[] {
88
+ const parts = profileTemplateParts(template)
89
+ if (typeof parts === "string") return []
90
+ return [...new Set(parts.flatMap((p) => ("secret" in p ? [p.secret] : [])))]
91
+ }
92
+
93
+ /** What to apply of a profile version, by the precedence rule above. */
94
+ export function planProfileApplication(req: ProfileApplyRequest, options: { readonly capabilities: ProfileCapabilities; readonly local?: ProfileLocalNames }): ProfilePlan {
95
+ const { capabilities: can } = options
96
+ const { content } = req
97
+ const localSkills = new Set(options.local?.skills ?? [])
98
+ const localCommands = new Set(options.local?.commands ?? [])
99
+ const notes: string[] = []
100
+ let offered = 0
101
+ let usable = 0
102
+
103
+ let instructions: string | undefined
104
+ if (content.instructions !== undefined && content.instructions.trim() !== "") {
105
+ offered++
106
+ if (can.instructions) {
107
+ instructions = profileInstructionsBlock(req)
108
+ usable++
109
+ } else notes.push("instructions not supported by this harness adapter")
110
+ }
111
+
112
+ const skills: ProfileSkill[] = []
113
+ if (content.skills.length) {
114
+ offered++
115
+ if (!can.skills) notes.push("skills not supported by this harness adapter")
116
+ else
117
+ for (const skill of content.skills) {
118
+ if (localSkills.has(skill.name)) notes.push(`skill ${skill.name} skipped: a local skill of that name takes precedence`)
119
+ else skills.push(skill)
120
+ }
121
+ if (skills.length) usable++
122
+ }
123
+
124
+ const commands: ProfileCommand[] = []
125
+ if (content.commands.length) {
126
+ offered++
127
+ if (!can.commands) notes.push("commands not supported by this harness adapter")
128
+ else
129
+ for (const command of content.commands) {
130
+ const secrets = templateSecrets(command.template)
131
+ if (localCommands.has(command.name)) notes.push(`command ${command.name} skipped: a local command of that name takes precedence`)
132
+ else if (secrets.length && !can.renderAtInvocation) notes.push(`command ${command.name} skipped: it needs secrets (${secrets.join(", ")}) and this adapter cannot render at invocation`)
133
+ else commands.push(command)
134
+ }
135
+ if (commands.length) usable++
136
+ }
137
+
138
+ let model: string | undefined
139
+ if (content.model !== undefined) {
140
+ offered++
141
+ if (can.model) {
142
+ model = content.model
143
+ usable++
144
+ } else notes.push("model ignored: this harness adapter cannot change the model")
145
+ }
146
+
147
+ const status: ProfileApplyStatus = offered > 0 && usable === 0 ? "unsupported" : !can.live ? "restart_required" : notes.length ? "partial" : "applied"
148
+ return { status, ...(instructions !== undefined ? { instructions } : {}), skills, commands, ...(model !== undefined ? { model } : {}), notes: boundedNotes(notes) }
149
+ }
150
+
151
+ /** What an adapter's apply may report; its status (if any) and notes replace or extend the plan's. */
152
+ export interface ProfileApplyOutcome {
153
+ readonly status?: ProfileApplyStatus
154
+ readonly notes?: readonly string[]
155
+ }
156
+
157
+ export interface ProfileApplyHandlerOptions {
158
+ readonly capabilities: ProfileCapabilities
159
+ /** The session's local skill and command names (they win over the profile's). */
160
+ readonly local?: (sessionID: string) => ProfileLocalNames | undefined
161
+ /** Writes the plan to the session's own (session-scoped) state. Throwing answers `rejected` with the reason. */
162
+ readonly apply: (plan: ProfilePlan, req: ProfileApplyRequest) => Promise<ProfileApplyOutcome | void>
163
+ /** Sessions whose last applied digest is remembered (default 1000). */
164
+ readonly remembered?: number
165
+ }
166
+
167
+ /** The answer to a profileApply request, echoing what it was asked to apply. */
168
+ export function profileApplyResult(req: ProfileApplyRequest, status: ProfileApplyStatus, notes: readonly string[] = []): ProfileApplyResult {
169
+ const bounded = boundedNotes(notes)
170
+ return { sessionID: req.sessionID, profileID: req.profileID, version: req.version, digest: req.digest, status, ...(bounded.length ? { notes: bounded } : {}) }
171
+ }
172
+
173
+ /**
174
+ * The `profileApply` op handler for HarnessSpoke (`handlers: { profileApply: handler.handle }`, which announces the
175
+ * feature): checks the digest, plans by precedence, calls the adapter, and answers a valid result. A repeated
176
+ * request for the digest a session already applied (applied or partial) answers the same result without
177
+ * re-applying; restart_required, rejected and unsupported are tried again.
178
+ */
179
+ export class ProfileApplyHandler {
180
+ private readonly options: ProfileApplyHandlerOptions
181
+ private readonly done = new Map<string, ProfileApplyResult>()
182
+
183
+ constructor(options: ProfileApplyHandlerOptions) {
184
+ this.options = options
185
+ }
186
+
187
+ readonly handle = async (req: ProfileApplyRequest): Promise<ProfileApplyResult> => {
188
+ if (profileDigest(req.content) !== req.digest) return profileApplyResult(req, "rejected", ["digest does not match the content; nothing applied"])
189
+ const previous = this.done.get(req.sessionID)
190
+ if (previous && previous.profileID === req.profileID && previous.version === req.version && previous.digest === req.digest) return previous
191
+ const plan = planProfileApplication(req, { capabilities: this.options.capabilities, ...(this.options.local ? { local: this.options.local(req.sessionID) ?? {} } : {}) })
192
+ if (plan.status === "unsupported") return profileApplyResult(req, "unsupported", plan.notes)
193
+ let outcome: ProfileApplyOutcome | void
194
+ try {
195
+ outcome = await this.options.apply(plan, req)
196
+ } catch (cause) {
197
+ return profileApplyResult(req, "rejected", [`adapter failed: ${cause instanceof Error ? cause.message : String(cause)}`])
198
+ }
199
+ const result = profileApplyResult(req, outcome?.status ?? plan.status, [...plan.notes, ...(outcome?.notes ?? [])])
200
+ this.done.delete(req.sessionID)
201
+ if (result.status === "applied" || result.status === "partial") {
202
+ this.done.set(req.sessionID, result)
203
+ while (this.done.size > (this.options.remembered ?? 1000)) this.done.delete(this.done.keys().next().value!)
204
+ }
205
+ return result
206
+ }
207
+
208
+ /** Forgets a session (it ended, or its applied state was lost). */
209
+ forget(sessionID: string): void {
210
+ this.done.delete(sessionID)
211
+ }
212
+ }
213
+
214
+ /** A command's secret references are not available on this machine (names only, never values). */
215
+ export class ProfileSecretMissingError extends Error {
216
+ constructor(readonly missing: readonly string[]) {
217
+ super(`missing secret(s) ${missing.join(", ")} on this machine`)
218
+ this.name = "ProfileSecretMissingError"
219
+ }
220
+ }
221
+
222
+ /** Secrets from the spoke's environment: `{{secret:NAME}}` reads `<prefix>NAME` (default HUB_SECRET_NAME). */
223
+ export function envSecrets(env: Readonly<Record<string, string | undefined>> = process.env, prefix = "HUB_SECRET_"): (name: string) => string | undefined {
224
+ return (name) => {
225
+ const value = env[`${prefix}${name}`]
226
+ return value === undefined || value === "" ? undefined : value
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Renders a command at invocation on the spoke: each `{{param}}` once, literally (a value is never parsed again),
232
+ * each `{{secret:NAME}}` from `secret`. Exactly the template's parameters must be given.
233
+ */
234
+ export function renderProfileCommand(template: string, params: Readonly<Record<string, string>>, secret: (name: string) => string | undefined): string {
235
+ const parts = profileTemplateParts(template)
236
+ if (typeof parts === "string") throw new Error(`template: ${parts}`)
237
+ const wanted = [...new Set(parts.flatMap((p) => ("param" in p ? [p.param] : [])))]
238
+ const missing = wanted.filter((name) => !Object.hasOwn(params, name))
239
+ if (missing.length) throw new Error(`missing parameter(s) ${missing.join(", ")}`)
240
+ const unknown = Object.keys(params).filter((name) => !wanted.includes(name))
241
+ if (unknown.length) throw new Error(`unknown parameter(s) ${unknown.map((n) => (/^[a-z][a-z0-9_]{0,39}$/.test(n) ? n : "?")).join(", ")}`)
242
+ const absent = [...new Set(parts.flatMap((p) => ("secret" in p && secret(p.secret) === undefined ? [p.secret] : [])))]
243
+ if (absent.length) throw new ProfileSecretMissingError(absent)
244
+ return parts.map((p) => ("text" in p ? p.text : "param" in p ? params[p.param]! : secret(p.secret)!)).join("")
245
+ }
@@ -73,6 +73,13 @@ export interface PromptInput {
73
73
  readonly images?: readonly PromptImage[]
74
74
  /** True for the user's own prompts; agent prompts never reset the hop chain (decision wvqeuis0 §8). */
75
75
  readonly resetHops: boolean
76
+ /**
77
+ * (CON-09a) false: the user did not write this text (a Concierge proposal run as the user), so it is never the
78
+ * source of the session's first-prompt intent. (CON-10) "first": it is the session's first prompt by construction
79
+ * (a launch's prompt, even when the launch already gave the session its name as title). Absent: it is the first
80
+ * prompt only when the session has no real title yet.
81
+ */
82
+ readonly intent?: false | "first"
76
83
  }
77
84
 
78
85
  /** Typed input steered into the running tree. */
@@ -14,6 +14,7 @@ import { DELIVER_ID_MAX, DELIVERY_MODES, type DeliveryMode, type FormAnswer, for
14
14
  import type { Acting } from "./acting"
15
15
  import { AgentError } from "./errors"
16
16
  import type { AgentRoute } from "./routes"
17
+ import { AGENT_JSON_BODY_MAX, boundedText } from "./body"
17
18
 
18
19
  // A literal: routes.ts imports this module (AGENT_PREFIX is not initialised yet when it loads).
19
20
  const PREFIX = "/api/agent/sessions/:addr"
@@ -27,7 +28,7 @@ const invalid = (message: string) => new AgentError("invalid", message)
27
28
  type Body = Record<string, unknown>
28
29
 
29
30
  async function bodyOf(request: Request, optional = false): Promise<Body> {
30
- const raw = await request.text()
31
+ const raw = await boundedText(request, AGENT_JSON_BODY_MAX)
31
32
  if (!raw.trim() && optional) return {}
32
33
  const value: unknown = JSON.parse(raw)
33
34
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalid("Body must be a JSON object")
@@ -6,6 +6,7 @@
6
6
  // (agent/acting.ts) calls ensureActing() first and again before the spoke request; it answers 423 paused.
7
7
  // Reading and messaging keep working (messaging has its own switch, AgentData.messagingPaused).
8
8
  // - hubNever(): hub.json `agents.never` (already validated by config.ts), checked before every grant.
9
+ import { boundedJson } from "./body"
9
10
  import type { Store } from "../store"
10
11
  import type { AgentActor } from "./actor"
11
12
  import type { AgentAudit } from "./audit"
@@ -82,7 +83,7 @@ export function actingSwitchRoutes(acting: ActingSwitch): AgentRoute[] {
82
83
  path: `${AGENT_PREFIX}/acting`,
83
84
  allow: {},
84
85
  async handle({ request, user, actor }) {
85
- const body: unknown = await request.json()
86
+ const body: unknown = await boundedJson(request)
86
87
  const enabled = typeof body === "object" && body !== null && !Array.isArray(body) ? (body as Record<string, unknown>).enabled : undefined
87
88
  if (typeof enabled !== "boolean") throw new AgentError("invalid", "enabled must be true or false")
88
89
  await acting.setUserActing(user.id, actor, enabled)