@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.
- package/CHANGELOG.md +430 -1
- package/Dockerfile +7 -2
- package/README.md +230 -32
- package/package.json +5 -3
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/daemon.ts +8 -1
- package/packages/claude-code/src/launcher.ts +29 -9
- package/packages/claude-code/src/paths.ts +3 -3
- package/packages/claude-code/src/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6871 -3460
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
- package/packages/cli/src/artifacts-mcp.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
- package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
- package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
- package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +96 -14
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +2 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/launches.ts +9 -3
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +385 -10
- package/packages/cli/src/messaging.ts +7 -3
- package/packages/cli/src/profiles-mcp.ts +301 -0
- package/packages/cli/src/setup/commands.ts +23 -4
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/spoke-services.ts +1 -1
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +460 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/paths.ts +3 -3
- package/packages/codex/src/profile.ts +163 -0
- package/packages/codex/src/sessions.ts +12 -0
- package/packages/codex/src/spoke.ts +19 -1
- package/packages/node/package.json +1 -1
- package/packages/node/src/config.ts +104 -2
- package/packages/node/src/opencode-warm.ts +344 -0
- package/packages/node/src/run.ts +28 -1
- package/packages/preset/package.json +1 -1
- package/packages/preset/src/profile.ts +34 -0
- package/packages/preset/src/spoke.ts +4 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/artifacts.ts +142 -0
- package/packages/protocol/src/concierge.ts +366 -0
- package/packages/protocol/src/contracts.ts +104 -0
- package/packages/protocol/src/graph.ts +70 -0
- package/packages/protocol/src/hub-workspace.ts +201 -0
- package/packages/protocol/src/index.ts +175 -6
- package/packages/protocol/src/launch.ts +36 -0
- package/packages/protocol/src/profiles.ts +301 -0
- package/packages/protocol/src/teams.ts +204 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/address.ts +50 -2
- package/packages/sdk/src/artifact-jobs.ts +194 -0
- package/packages/sdk/src/artifacts.ts +187 -0
- package/packages/sdk/src/client.ts +226 -5
- package/packages/sdk/src/concierge.ts +88 -0
- package/packages/sdk/src/errors.ts +4 -1
- package/packages/sdk/src/index.ts +6 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +568 -1
- package/packages/sdk/src/wake.ts +20 -0
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/index.ts +1 -0
- package/packages/spoke-sdk/src/profile-apply.ts +245 -0
- package/src/central/actions.ts +7 -0
- package/src/central/agent/acting-routes.ts +2 -1
- package/src/central/agent/acting-switch.ts +2 -1
- package/src/central/agent/acting.ts +218 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +18 -1
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +125 -2
- package/src/central/agent/delivery.ts +194 -40
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +7 -0
- package/src/central/agent/escalation.ts +10 -2
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +18 -4
- package/src/central/agent/form-check.ts +85 -0
- package/src/central/agent/grant-requests.ts +2 -1
- package/src/central/agent/grant-routes.ts +4 -3
- package/src/central/agent/grants.ts +29 -2
- package/src/central/agent/idempotency.ts +133 -0
- package/src/central/agent/intent-routes.ts +116 -0
- package/src/central/agent/intents.ts +505 -0
- package/src/central/agent/interaction-graph-routes.ts +212 -0
- package/src/central/agent/interaction-graph.ts +865 -0
- package/src/central/agent/launch-engine.ts +189 -32
- package/src/central/agent/launch-routes.ts +26 -10
- package/src/central/agent/launch-spec.ts +40 -3
- package/src/central/agent/launch-store.ts +55 -2
- package/src/central/agent/message-limits.ts +89 -11
- package/src/central/agent/message-routes.ts +10 -4
- package/src/central/agent/messages.ts +28 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/placement.ts +93 -18
- package/src/central/agent/read-routes.ts +89 -19
- package/src/central/agent/routes.ts +178 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- package/src/central/agent/webhooks.ts +17 -2
- package/src/central/agent-data.ts +4 -0
- package/src/central/artifacts/access.ts +44 -0
- package/src/central/artifacts/blob-store.ts +113 -0
- package/src/central/artifacts/blob-support.ts +98 -0
- package/src/central/artifacts/events.ts +27 -0
- package/src/central/artifacts/host-caps.ts +162 -0
- package/src/central/artifacts/host.ts +315 -0
- package/src/central/artifacts/idempotency.ts +109 -0
- package/src/central/artifacts/jobs-routes.ts +776 -0
- package/src/central/artifacts/jobs.ts +344 -0
- package/src/central/artifacts/maintenance.ts +103 -0
- package/src/central/artifacts/routes.ts +360 -0
- package/src/central/artifacts/store.ts +776 -0
- package/src/central/artifacts/workspaces.ts +31 -0
- package/src/central/backend/file-blobs.ts +377 -0
- package/src/central/backend/index.ts +12 -3
- package/src/central/backend/memory-blobs.ts +116 -0
- package/src/central/backend/postgres-blobs.ts +513 -0
- package/src/central/backend/postgres-migrate.ts +33 -0
- package/src/central/backend/postgres-state.ts +19 -0
- package/src/central/backend/types.ts +8 -0
- package/src/central/concierge/activity.ts +142 -0
- package/src/central/concierge/arm.ts +63 -0
- package/src/central/concierge/board-render.ts +190 -0
- package/src/central/concierge/board-results.ts +169 -0
- package/src/central/concierge/board-routes.ts +350 -0
- package/src/central/concierge/board.ts +249 -0
- package/src/central/concierge/context.ts +221 -0
- package/src/central/concierge/entitlements.ts +13 -0
- package/src/central/concierge/guard.ts +120 -0
- package/src/central/concierge/inbound.ts +86 -0
- package/src/central/concierge/index.ts +84 -0
- package/src/central/concierge/notes.ts +41 -0
- package/src/central/concierge/notifier.ts +986 -0
- package/src/central/concierge/prompt.ts +131 -0
- package/src/central/concierge/proposals.ts +268 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +122 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +125 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +767 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +279 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +28 -6
- package/src/central/http.ts +660 -54
- package/src/central/main.ts +1 -1
- package/src/central/mcp-http.ts +10 -21
- package/src/central/profiles/apply.ts +333 -0
- package/src/central/profiles/content.ts +174 -0
- package/src/central/profiles/events.ts +40 -0
- package/src/central/profiles/idempotency.ts +14 -0
- package/src/central/profiles/import.ts +139 -0
- package/src/central/profiles/routes.ts +356 -0
- package/src/central/profiles/store.ts +597 -0
- package/src/central/profiles/usage-ports.ts +120 -0
- package/src/central/profiles/usage.ts +364 -0
- package/src/central/push.ts +7 -0
- package/src/central/registry.ts +69 -7
- package/src/central/store.ts +4 -0
- package/src/central/teams/collections.ts +34 -0
- package/src/central/teams/discovery.ts +346 -0
- package/src/central/teams/draft-edits.ts +46 -0
- package/src/central/teams/events.ts +46 -0
- package/src/central/teams/intake-routes.ts +100 -0
- package/src/central/teams/intake.ts +786 -0
- package/src/central/teams/policy.ts +231 -0
- package/src/central/teams/routes.ts +516 -0
- package/src/central/teams/seats.ts +272 -0
- package/src/central/teams/staffing.ts +910 -0
- package/src/central/teams/store.ts +667 -0
- package/src/central/teams/validation.ts +256 -0
- package/src/central/workspaces/board.ts +408 -0
- package/src/central/workspaces/checkpoints.ts +316 -0
- package/src/central/workspaces/coordinator.ts +841 -0
- package/src/central/workspaces/events.ts +85 -0
- package/src/central/workspaces/lifecycle.ts +93 -0
- package/src/central/workspaces/master.ts +490 -0
- package/src/central/workspaces/members.ts +632 -0
- package/src/central/workspaces/routes.ts +354 -0
- package/src/central/workspaces/sidechats-routes.ts +410 -0
- package/src/central/workspaces/sidechats.ts +482 -0
- package/src/central/workspaces/store.ts +571 -0
- package/src/central/workspaces/supervision.ts +355 -0
- package/src/index.ts +126 -23
- package/src/migrate.ts +109 -3
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/env-inject.ts +8 -1
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +404 -25
- package/src/spoke/launchers.ts +6 -4
- package/src/spoke/listing.ts +65 -10
- package/src/spoke/localhub.ts +14 -4
- package/src/spoke/locations.ts +40 -0
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +256 -0
- package/src/spoke/spoke.ts +330 -23
- package/src/spoke/timeline.ts +6 -1
- package/src/spoke/tracker.ts +23 -1
- package/src/spoke/who-teams.ts +81 -0
- package/src/tui-messaging.ts +32 -18
- package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
- package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
- package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
- package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
- package/web/dist/assets/index-BabrNDB3.css +1 -0
- package/web/dist/assets/index-lIOfSYck.js +34 -0
- package/web/dist/assets/live-BDq0AABg.js +5 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
- package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
- package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
- package/web/dist/assets/source-CDU04kYw.js +1 -0
- package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
- package/web/dist/index.html +5 -2
- package/web/dist/sw.js +22 -2
- package/web/dist/assets/index-Dm8OmkaO.css +0 -1
- 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
|
+
}
|
package/src/central/actions.ts
CHANGED
|
@@ -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
|
|
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
|
|
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)
|