@huynle/hub 0.9.1 → 0.10.0
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 +309 -0
- package/Dockerfile +5 -1
- package/README.md +153 -77
- package/package.json +7 -4
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +532 -261
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/cli.ts +36 -1
- package/packages/cli/src/subscriptions.ts +49 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/sessions.ts +10 -6
- package/packages/codex/src/spoke.ts +2 -1
- package/packages/node/package.json +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/schema/app-delivery/v1.schema.json +7 -5
- package/packages/protocol/schema/app-front/v1.schema.json +410 -0
- package/packages/protocol/src/app-front.ts +293 -0
- package/packages/protocol/src/concierge.ts +10 -1
- package/packages/protocol/src/context-cards.ts +59 -0
- package/packages/protocol/src/credentials.ts +39 -2
- package/packages/protocol/src/index.ts +69 -11
- 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/app-front.ts +524 -0
- package/packages/sdk/src/client.ts +37 -4
- package/packages/sdk/src/envelope.ts +9 -2
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/index.ts +1 -0
- package/packages/sdk/src/types.ts +39 -1
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/delivery.ts +17 -1
- package/packages/spoke-sdk/src/harness-spoke.ts +4 -3
- package/packages/voice-client/README.md +146 -0
- package/packages/voice-client/src/vad/LICENSE +21 -0
- package/src/central/agent/acting-routes.ts +17 -3
- package/src/central/agent/acting.ts +12 -3
- package/src/central/agent/actor.ts +6 -1
- package/src/central/agent/app-decisions.ts +146 -0
- package/src/central/agent/app-tokens.ts +9 -3
- package/src/central/agent/audit.ts +3 -0
- package/src/central/agent/connect-routes.ts +64 -0
- package/src/central/agent/connect.ts +868 -0
- package/src/central/agent/conversation.ts +625 -0
- package/src/central/agent/decisions.ts +71 -6
- package/src/central/agent/delivery.ts +3 -0
- package/src/central/agent/directory.ts +5 -0
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/grant-requests.ts +46 -9
- package/src/central/agent/grants.ts +13 -2
- package/src/central/agent/message-limits.ts +2 -2
- package/src/central/agent/message-routes.ts +13 -4
- package/src/central/agent/read-routes.ts +16 -2
- package/src/central/agent/routes.ts +108 -6
- package/src/central/agent/speech-routes.ts +316 -0
- package/src/central/agent/subscription-routes.ts +174 -0
- package/src/central/agent/subscriptions.ts +980 -0
- package/src/central/agent/webhooks.ts +5 -2
- package/src/central/agent-data.ts +43 -1
- package/src/central/backend/memory-counters.ts +2 -2
- package/src/central/backend/types.ts +5 -2
- package/src/central/backend/valkey-counters.ts +5 -4
- package/src/central/client-address.ts +16 -0
- package/src/central/concierge/board-render.ts +52 -1
- package/src/central/concierge/board-routes.ts +25 -2
- package/src/central/concierge/board.ts +11 -1
- package/src/central/concierge/context.ts +29 -8
- package/src/central/concierge/grant-request.ts +216 -0
- package/src/central/concierge/prompt.ts +49 -2
- package/src/central/concierge/session-routes.ts +3 -0
- package/src/central/config.ts +88 -5
- package/src/central/context-cards/budget.ts +121 -0
- package/src/central/context-cards/changes.ts +29 -0
- package/src/central/context-cards/config.ts +145 -0
- package/src/central/context-cards/engine.ts +379 -0
- package/src/central/context-cards/inputs.ts +111 -0
- package/src/central/context-cards/pickup.ts +134 -0
- package/src/central/context-cards/registry.ts +89 -0
- package/src/central/context-cards/routes.ts +37 -0
- package/src/central/context-cards/service.ts +367 -0
- package/src/central/context-cards/store.ts +67 -0
- package/src/central/context-cards/types.ts +88 -0
- package/src/central/fetcher.ts +10 -2
- package/src/central/front-relays.ts +423 -0
- package/src/central/http.ts +517 -56
- package/src/central/human-activity.ts +453 -0
- package/src/central/llm/chat.ts +133 -0
- package/src/central/push-target.ts +11 -2
- package/src/central/registry.ts +162 -9
- package/src/central/speech/service.ts +9 -0
- package/src/central/spoke-ws.ts +13 -0
- package/src/central/store.ts +9 -12
- package/src/config.ts +30 -13
- package/src/index.ts +45 -43
- package/src/rpc.ts +33 -4
- package/src/spoke/concierge-agent.ts +1 -1
- package/src/spoke/conversation.ts +6 -75
- package/src/spoke/deliver.ts +18 -8
- package/src/spoke/hub-rpc.ts +7 -2
- package/src/spoke/hub-tool-subscriptions.ts +82 -0
- package/src/spoke/hub-tool-title.ts +89 -0
- package/src/spoke/hub-tool.ts +56 -18
- package/src/spoke/session-context.ts +10 -2
- package/src/spoke/spoke.ts +209 -238
- package/src/spoke/summary.ts +36 -1
- package/src/spoke/taskboard.ts +5 -6
- package/src/spoke/voice-front.ts +323 -30
- package/src/spoke/who-teams.ts +51 -2
- package/src/tui-messaging.ts +24 -10
- package/src/tui.ts +6 -6
- package/web/dist/assets/{ArtifactPage-BKISN-Qj.js → ArtifactPage-Bz992ZX6.js} +1 -1
- package/web/dist/assets/{ArtifactPane-CKOiAUNH.js → ArtifactPane-BJZNixJC.js} +1 -1
- package/web/dist/assets/ArtifactWorkbench-Dti3VP07.js +6 -0
- package/web/dist/assets/{Diagram-vdpmp3xu.css → Diagram-7y8kg3og.css} +1 -1
- package/web/dist/assets/Diagram-B4HdUxw0.js +1 -0
- package/web/dist/assets/{Library-CcjZXsfF.js → Library-CpzD2HDW.js} +2 -2
- package/web/dist/assets/Lightbox-BlbivCkd.js +1 -0
- package/web/dist/assets/MediaImage-Ch3ryIdR.js +4 -0
- package/web/dist/assets/ProfilesDestination-DlQeogea.js +1 -0
- package/web/dist/assets/{TeamsPage-CDyjrK-I.js → TeamsPage-4xdWCaI-.js} +1 -1
- package/web/dist/assets/{Watchtower-DYwoUWME.js → Watchtower-CpQdIRns.js} +2 -2
- package/web/dist/assets/{artifacts-DU9uRgs1.js → artifacts-But87Jfo.js} +1 -1
- package/web/dist/assets/index-BC0O1gfR.css +1 -0
- package/web/dist/assets/index-BS6lA6fN.js +33 -0
- package/web/dist/assets/{live-ycZ_aQZO.js → live-CVUFIdUX.js} +2 -2
- package/web/dist/assets/{pane-layout-0RrLHGDr.js → pane-layout-BW0RdX5a.js} +1 -1
- package/web/dist/assets/{renderer2d-BIwbgRoX.js → renderer2d-IaJiloPi.js} +1 -1
- package/web/dist/assets/{renderer3d-C1a7eYGQ.js → renderer3d-ZiD1uKGW.js} +1 -1
- package/web/dist/assets/{safe-area-DHhPy61Q.js → safe-area-C7IAlkgY.js} +1 -1
- package/web/dist/assets/{source-BWlvaODa.js → source-C-De8L3s.js} +1 -1
- package/web/dist/assets/toast-CXTwTvDw.js +9 -0
- package/web/dist/assets/vad-worklet-BfxJhrXy.js +27 -0
- package/web/dist/index.html +6 -6
- package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +0 -6
- package/web/dist/assets/Diagram-azw2B_QH.js +0 -1
- package/web/dist/assets/Lightbox-Du56MXGX.js +0 -1
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +0 -4
- package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +0 -1
- package/web/dist/assets/index-C3ci5UtY.css +0 -1
- package/web/dist/assets/index-LNMZCqgE.js +0 -36
- package/web/dist/assets/toast-Cuu2K0hx.js +0 -1
- /package/{web/src/voice → packages/voice-client/src}/turn/LICENSE +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// The session subscription API on /api/agent (decision 74qawely; docs/subscriptions.md), over Subscriptions
|
|
2
|
+
// (subscriptions.ts):
|
|
3
|
+
//
|
|
4
|
+
// POST /api/agent/subscriptions {subscriber?, to, types?, delivery?, batch?, maxPerHour?, expires?, note?}
|
|
5
|
+
// → 201 {subscription}
|
|
6
|
+
// GET /api/agent/subscriptions?session=<addr> → {subscriptions}
|
|
7
|
+
// GET /api/agent/subscriptions/:id → {subscription}
|
|
8
|
+
// DELETE /api/agent/subscriptions/:id → 204
|
|
9
|
+
//
|
|
10
|
+
// Who: a session token subscribes ITSELF only (`subscriber` absent or naming itself, else 403 forbidden) and sees and
|
|
11
|
+
// removes only the subscriptions it is the subscriber of. Users manage all of theirs (`subscriber` required; `?session=`
|
|
12
|
+
// lists what a session subscribes to and what subscribes to it). App tokens need events:subscribe and sessions:read,
|
|
13
|
+
// plus the scope of each selected family (messages:read, sessions:launch, sessions:manage) and, for any delivery but
|
|
14
|
+
// silent (which never wakes or steers), messages:send — as for POST /msg; they see and remove only
|
|
15
|
+
// the subscriptions they created. A subscription that is not the caller's is 404 not_found, like a missing one.
|
|
16
|
+
// Notices always go to the subscriber session and respect ITS visibility, whoever created the subscription.
|
|
17
|
+
import { boundedJson } from "./body"
|
|
18
|
+
import type { Store } from "../store"
|
|
19
|
+
import type { AgentActor } from "./actor"
|
|
20
|
+
import type { Directory } from "./directory"
|
|
21
|
+
import { AgentError } from "./errors"
|
|
22
|
+
import type { AgentRoute } from "./routes"
|
|
23
|
+
import { parseSubscriptionTypes, SUBSCRIPTION_EVENT_TYPES, SUBSCRIPTION_FAMILIES, type SubscriptionOwner, type Subscriptions, typeMatches } from "./subscriptions"
|
|
24
|
+
import { type Census, type TeamDiscovery, teamScoped, visible } from "../teams/discovery"
|
|
25
|
+
|
|
26
|
+
// A literal: routes.ts imports this module.
|
|
27
|
+
const PATH = "/api/agent/subscriptions"
|
|
28
|
+
const NO_STORE = { "cache-control": "no-store" }
|
|
29
|
+
/** The scopes every app subscription needs. */
|
|
30
|
+
export const SUBSCRIPTION_APP_SCOPES: readonly string[] = ["events:subscribe", "sessions:read"]
|
|
31
|
+
|
|
32
|
+
export type RouteSubscriptions = Pick<Subscriptions, "create" | "list" | "get" | "remove">
|
|
33
|
+
|
|
34
|
+
export interface SubscriptionRouteDeps {
|
|
35
|
+
readonly subscriptions: RouteSubscriptions
|
|
36
|
+
readonly directory: Pick<Directory, "resolve">
|
|
37
|
+
/** Address resolution with team visibility (HWA-40); absent: the directory. */
|
|
38
|
+
readonly discovery?: Pick<TeamDiscovery, "census" | "resolve">
|
|
39
|
+
readonly store?: Pick<Store, "audit">
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The scope an app needs per type (computed on use: routes.ts and subscriptions.ts import each other through events-routes). */
|
|
43
|
+
const scopeOf = (type: string): string | undefined =>
|
|
44
|
+
type === "message.changed" ? "messages:read" : type === "launch.changed" ? "sessions:launch" : [...(SUBSCRIPTION_FAMILIES.naming ?? []), ...(SUBSCRIPTION_FAMILIES.manage ?? [])].includes(type) ? "sessions:manage" : undefined
|
|
45
|
+
|
|
46
|
+
/** The scopes an app needs for these type patterns beyond SUBSCRIPTION_APP_SCOPES (state qualifiers: none). */
|
|
47
|
+
export function subscriptionTypeScopes(patterns: readonly string[]): string[] {
|
|
48
|
+
const scopes = new Set<string>()
|
|
49
|
+
for (const type of SUBSCRIPTION_EVENT_TYPES) {
|
|
50
|
+
const scope = scopeOf(type)
|
|
51
|
+
if (scope && patterns.some((p) => !p.startsWith("state:") && typeMatches([p], { type, data: {} }))) scopes.add(scope)
|
|
52
|
+
}
|
|
53
|
+
return [...scopes]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const notFound = (id: string) => new AgentError("not_found", `No subscription ${id}`)
|
|
57
|
+
|
|
58
|
+
const ownerOf = (actor: AgentActor): SubscriptionOwner => (actor.kind === "app" ? `app:${actor.tokenID}` : actor.kind === "session" ? `ses:${actor.sessionID}` : "user")
|
|
59
|
+
|
|
60
|
+
/** What a caller may see and remove: an app what it created, a session what it subscribes, the user everything. */
|
|
61
|
+
const onlyFor = (actor: AgentActor): { by?: SubscriptionOwner; subscriber?: string } =>
|
|
62
|
+
actor.kind === "app" ? { by: ownerOf(actor) } : actor.kind === "session" ? { subscriber: actor.sessionID } : {}
|
|
63
|
+
|
|
64
|
+
function requireAppScopes(actor: AgentActor, extra: readonly string[] = []): void {
|
|
65
|
+
if (actor.kind !== "app") return
|
|
66
|
+
const missing = [...SUBSCRIPTION_APP_SCOPES, ...extra].filter((scope) => !actor.scopes.includes(scope))
|
|
67
|
+
if (missing.length) throw new AgentError("forbidden", `Subscriptions need scope ${missing.join(", ")}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function bodyOf(request: Request): Promise<Record<string, unknown>> {
|
|
71
|
+
const raw = (await boundedJson(request)) as unknown
|
|
72
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new AgentError("invalid", "Body must be a JSON object")
|
|
73
|
+
return raw as Record<string, unknown>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The sessions of a team (by name or ID) the census's caller may see; none when it may not see the team. */
|
|
77
|
+
export function teamMembers(census: Census, name: string): string[] {
|
|
78
|
+
const team = census.teams.get(name) ?? [...census.teams.values()].find((t) => t.name === name)
|
|
79
|
+
if (!team || (teamScoped(census) && !census.callerTeams.has(team.id))) return []
|
|
80
|
+
return [...census.seats].filter(([sessionID, seats]) => seats.some((s) => s.teamID === team.id) && visible(census, sessionID)).map(([sessionID]) => sessionID)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Subscriptions' resolveTarget with team visibility: the sessions an address (`#team` included) names for the
|
|
85
|
+
* subscriber session. Without discovery: the directory (and no teams).
|
|
86
|
+
*/
|
|
87
|
+
export function subscriptionResolver(directory: Pick<Directory, "resolve">, discovery?: Pick<TeamDiscovery, "census" | "resolve">) {
|
|
88
|
+
return async (userID: string, subscriberID: string, addr: string): Promise<string[]> => {
|
|
89
|
+
if (!discovery) return addr.startsWith("#") ? [] : directory.resolve(userID, addr).sessions.map((s) => s.sessionID)
|
|
90
|
+
const census = await discovery.census(userID, { kind: "session", userID, machineID: "", sessionID: subscriberID, via: "shell" })
|
|
91
|
+
if (addr.startsWith("#")) return teamMembers(census, addr.slice(1))
|
|
92
|
+
return discovery.resolve(census, addr).sessions.map((s) => s.sessionID)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function subscriptionRoutes(deps: SubscriptionRouteDeps): AgentRoute[] {
|
|
97
|
+
/** The one session an address names for the caller (a role with several holders is ambiguous). */
|
|
98
|
+
const one = async (userID: string, actor: AgentActor, addr: string): Promise<string> => {
|
|
99
|
+
const sessions = deps.discovery ? deps.discovery.resolve(await deps.discovery.census(userID, actor), addr).sessions : deps.directory.resolve(userID, addr).sessions
|
|
100
|
+
if (sessions.length !== 1) throw new AgentError("ambiguous", `${addr} names ${sessions.length} sessions`, { extra: { candidates: sessions.slice(0, 20).map((s) => ({ sessionID: s.sessionID, ...(s.name ? { name: s.name } : {}) })) } })
|
|
101
|
+
return sessions[0]!.sessionID
|
|
102
|
+
}
|
|
103
|
+
const ALLOW = { session: true, app: true } as const
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
method: "POST",
|
|
107
|
+
path: PATH,
|
|
108
|
+
allow: ALLOW,
|
|
109
|
+
async handle({ request, actor, user }) {
|
|
110
|
+
requireAppScopes(actor)
|
|
111
|
+
const input = await bodyOf(request)
|
|
112
|
+
// Waking or steering a session is a send: an app needs messages:send for any delivery but silent.
|
|
113
|
+
requireAppScopes(actor, [...subscriptionTypeScopes(parseSubscriptionTypes(input.types)), ...(input.delivery === "silent" ? [] : ["messages:send"])])
|
|
114
|
+
let subscriber: string
|
|
115
|
+
if (actor.kind === "session") {
|
|
116
|
+
subscriber = actor.sessionID
|
|
117
|
+
if (input.subscriber !== undefined && input.subscriber !== null) {
|
|
118
|
+
if (typeof input.subscriber !== "string" || (await one(user.id, actor, input.subscriber)) !== actor.sessionID) throw new AgentError("forbidden", "A session subscribes only itself", { hint: "Leave subscriber out" })
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (typeof input.subscriber !== "string" || !input.subscriber.trim()) throw new AgentError("invalid", "subscriber is required: the session that gets the notices (@name, ses_…)")
|
|
122
|
+
subscriber = await one(user.id, actor, input.subscriber.trim())
|
|
123
|
+
}
|
|
124
|
+
const result = await deps.subscriptions.create(user.id, ownerOf(actor), {
|
|
125
|
+
subscriber,
|
|
126
|
+
to: input.to,
|
|
127
|
+
types: input.types,
|
|
128
|
+
delivery: input.delivery,
|
|
129
|
+
batch: input.batch,
|
|
130
|
+
maxPerHour: input.maxPerHour,
|
|
131
|
+
expires: input.expires,
|
|
132
|
+
note: input.note,
|
|
133
|
+
})
|
|
134
|
+
await deps.store?.audit({ actor: user.id, action: "subscription.create", target: result.subscription.id, detail: subscriber })
|
|
135
|
+
return Response.json(result, { status: 201, headers: NO_STORE })
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
method: "GET",
|
|
140
|
+
path: PATH,
|
|
141
|
+
allow: ALLOW,
|
|
142
|
+
async handle({ url, actor, user }) {
|
|
143
|
+
requireAppScopes(actor)
|
|
144
|
+
const addr = url.searchParams.get("session")
|
|
145
|
+
const about = addr ? await one(user.id, actor, addr) : undefined
|
|
146
|
+
const subscriptions = await deps.subscriptions.list(user.id, { ...onlyFor(actor), ...(about !== undefined ? { about } : {}) })
|
|
147
|
+
return Response.json({ subscriptions }, { headers: NO_STORE })
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
method: "GET",
|
|
152
|
+
path: `${PATH}/:id`,
|
|
153
|
+
allow: ALLOW,
|
|
154
|
+
async handle({ params, actor, user }) {
|
|
155
|
+
requireAppScopes(actor)
|
|
156
|
+
const found = await deps.subscriptions.get(user.id, params.id!)
|
|
157
|
+
const only = onlyFor(actor)
|
|
158
|
+
if (!found || (only.by !== undefined && found.by !== only.by) || (only.subscriber !== undefined && found.subscriber !== only.subscriber)) throw notFound(params.id!)
|
|
159
|
+
return Response.json({ subscription: found }, { headers: NO_STORE })
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
method: "DELETE",
|
|
164
|
+
path: `${PATH}/:id`,
|
|
165
|
+
allow: ALLOW,
|
|
166
|
+
async handle({ params, actor, user }) {
|
|
167
|
+
requireAppScopes(actor)
|
|
168
|
+
if (!(await deps.subscriptions.remove(user.id, params.id!, onlyFor(actor)))) throw notFound(params.id!)
|
|
169
|
+
await deps.store?.audit({ actor: user.id, action: "subscription.delete", target: params.id! })
|
|
170
|
+
return new Response(null, { status: 204, headers: NO_STORE })
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
]
|
|
174
|
+
}
|