@huynle/hub 0.10.1 → 0.11.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 +54 -0
- package/README.md +17 -3
- package/package.json +1 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +622 -394
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/grants-mcp.ts +65 -0
- package/packages/cli/src/mcp-operations.ts +49 -0
- package/packages/cli/src/mcp.ts +6 -0
- package/packages/cli/src/subscriptions-mcp.ts +76 -0
- package/packages/cli/src/teams-mcp.ts +5 -3
- package/packages/codex/package.json +1 -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/src/teams.ts +4 -0
- package/packages/protocol/src/voice-recording.ts +39 -2
- package/packages/runtime-openshell/image/entry.sh +3 -1
- package/packages/runtime-openshell/image/start.sh +3 -1
- 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/client.ts +13 -0
- package/packages/sdk/src/teams.ts +2 -0
- package/packages/spoke-sdk/package.json +1 -1
- package/src/central/agent/decisions.ts +7 -1
- package/src/central/agent/delivery.ts +39 -9
- package/src/central/agent/grant-routes.ts +14 -1
- package/src/central/agent/grants.ts +23 -0
- package/src/central/agent/launch-engine.ts +3 -3
- package/src/central/agent/message-limits.ts +3 -0
- package/src/central/agent/routes.ts +11 -0
- package/src/central/agent/subscriptions.ts +3 -1
- package/src/central/config.ts +9 -2
- package/src/central/http.ts +102 -20
- package/src/central/recording/export.ts +57 -3
- package/src/central/recording/index.ts +234 -16
- package/src/central/recording/ingest.ts +11 -2
- package/src/central/recording/routes.ts +61 -7
- package/src/central/recording/schema.ts +66 -2
- package/src/central/recording/settings.ts +19 -5
- package/src/central/recording/store.ts +19 -0
- package/src/central/recording/taps.ts +45 -1
- package/src/central/speech/openai.ts +4 -4
- package/src/central/speech/openrouter.ts +4 -4
- package/src/central/speech/provider.ts +32 -1
- package/src/central/speech/service.ts +6 -5
- package/src/central/teams/hop-stalls.ts +72 -0
- package/src/central/teams/policy.ts +10 -0
- package/src/central/teams/routes.ts +2 -0
- package/src/central/teams/seats.ts +3 -3
- package/src/central/teams/staffing.ts +5 -0
- package/src/central/teams/store.ts +5 -2
- package/src/central/teams/validation.ts +8 -5
- package/web/dist/assets/{ArtifactPage-CsPjam8D.js → ArtifactPage-C5MMqXLN.js} +1 -1
- package/web/dist/assets/{ArtifactPane-B9weO8L6.js → ArtifactPane-CXSCmhyJ.js} +1 -1
- package/web/dist/assets/{ArtifactWorkbench-DzKhMs3l.js → ArtifactWorkbench-S0RxNGWy.js} +1 -1
- package/web/dist/assets/{Diagram-CuRIffka.js → Diagram-BGmyRb-R.js} +1 -1
- package/web/dist/assets/{Library-rv5DGtrm.js → Library-CFmMbXEa.js} +2 -2
- package/web/dist/assets/{ProfilesDestination-DUuQ9jmd.js → ProfilesDestination-7jpOc2HM.js} +1 -1
- package/web/dist/assets/{TeamsPage-D_17MkvN.js → TeamsPage-C1NGrUR-.js} +1 -1
- package/web/dist/assets/{Watchtower-DlrNOJ7X.js → Watchtower-DnvlJaU0.js} +2 -2
- package/web/dist/assets/{artifacts-But87Jfo.js → artifacts-DlQEYmDl.js} +1 -1
- package/web/dist/assets/index-Da8SKXMw.js +33 -0
- package/web/dist/assets/{index-rW6LKQj1.css → index-Dyq-jBeE.css} +1 -1
- package/web/dist/assets/live-BmqLASSo.js +5 -0
- package/web/dist/assets/{pane-layout-BW0RdX5a.js → pane-layout-BAuu_pA3.js} +1 -1
- package/web/dist/assets/{renderer2d-DyANQ1WS.js → renderer2d-B2Nh39UN.js} +1 -1
- package/web/dist/assets/{renderer3d-vYYKFAJq.js → renderer3d-CTYbljHH.js} +1 -1
- package/web/dist/assets/{safe-area-B2Bk__FK.js → safe-area-CkhEclGo.js} +1 -1
- package/web/dist/assets/{silero-worker-e802lB5T.js → silero-worker-CC2etKS3.js} +1 -1
- package/web/dist/assets/{source-C-De8L3s.js → source-GYayxUxS.js} +1 -1
- package/web/dist/assets/{toast-CXTwTvDw.js → toast-q818mjUF.js} +1 -1
- package/web/dist/index.html +5 -5
- package/web/dist/assets/index-bX8B6NLo.js +0 -33
- package/web/dist/assets/live-BGLA-peO.js +0 -5
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// MCP grant tools: the server owns authority. Requests use the existing tap Decision route; /mine never widens
|
|
2
|
+
// the user-only grant administration routes. A user credential may create directly but has no /mine holder.
|
|
3
|
+
import type { HubClient } from "../../sdk/src/client.ts"
|
|
4
|
+
import type { GrantRequestSpec, GrantSpec } from "../../sdk/src/types.ts"
|
|
5
|
+
import { grantError, grantText, grantsText, GRANT_USER_HINT, requestedText } from "./grants"
|
|
6
|
+
import { CliError, EXIT } from "./http"
|
|
7
|
+
import { ToolArgError } from "./tool-args"
|
|
8
|
+
|
|
9
|
+
type Args = Record<string, unknown>
|
|
10
|
+
export interface GrantTool {
|
|
11
|
+
readonly name: string
|
|
12
|
+
readonly description: string
|
|
13
|
+
readonly properties: Record<string, unknown>
|
|
14
|
+
readonly required?: readonly string[]
|
|
15
|
+
readonly readOnly: boolean
|
|
16
|
+
readonly organizes?: boolean
|
|
17
|
+
run(hub: HubClient, args: Args, signal: AbortSignal): Promise<string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STRING = { type: "string" } as const
|
|
21
|
+
const GRANT = { type: "object", description: "The grant spec accepted by POST /api/agent/grants: holder (optional for a request), scope.projects, allow, permissions, allowAlways, maxActionsPerHour and expires. The server validates all fields.", additionalProperties: true } as const
|
|
22
|
+
const need = (args: Args, key: string): string => {
|
|
23
|
+
if (typeof args[key] !== "string" || !(args[key] as string).trim()) throw new ToolArgError(`"${key}" is required (a non-empty string)`)
|
|
24
|
+
return args[key] as string
|
|
25
|
+
}
|
|
26
|
+
const spec = (args: Args): Record<string, unknown> => {
|
|
27
|
+
if (typeof args.grant !== "object" || args.grant === null || Array.isArray(args.grant)) throw new ToolArgError('"grant" is required (an object)')
|
|
28
|
+
return args.grant as Record<string, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const grantTools: readonly GrantTool[] = [
|
|
32
|
+
{
|
|
33
|
+
name: "grant_request", description: "Ask the user for a grant: opens one tap Decision for @me (approve or deny). Sessions and apps with messages:send only; one open request and at most five per hour. Only the user's answer creates authority.",
|
|
34
|
+
properties: { grant: GRANT, why: STRING }, required: ["grant", "why"], readOnly: false,
|
|
35
|
+
run: async (hub, args, signal) => requestedText(await hub.requestGrant(spec(args) as GrantRequestSpec, need(args, "why"), { signal })),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "grant_list", description: "List grants (active, expired or revoked) whose holder is this session or app, including grants for roles it holds now. Apps need messages:read. A user credential has no holder: use the console or hub grant list for administration.",
|
|
39
|
+
properties: {}, readOnly: true,
|
|
40
|
+
run: async (hub, _args, signal) => grantsText((await hub.heldGrants({ signal })).grants),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "grant_show", description: "Show one grant only if this credential currently holds it. Apps need messages:read; another holder's ID answers not_found.",
|
|
44
|
+
properties: { id: STRING }, required: ["id"], readOnly: true,
|
|
45
|
+
run: async (hub, args, signal) => grantText((await hub.heldGrant(need(args, "id"), { signal })).grant),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "grant_revoke", description: "Irreversibly revoke a grant held by this session or app (including a held role shared with other holders). Apps need messages:send; another holder's ID answers not_found.",
|
|
49
|
+
properties: { id: STRING }, required: ["id"], readOnly: false,
|
|
50
|
+
run: async (hub, args, signal) => grantText((await hub.revokeHeldGrant(need(args, "id"), { signal })).grant),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "grant_create", description: `Create a grant directly ONLY with the user's device credential. Sessions and apps must use grant_request and wait for the user's tap. ${GRANT_USER_HINT}.`,
|
|
54
|
+
properties: { grant: GRANT }, required: ["grant"], readOnly: false,
|
|
55
|
+
run: async (hub, args, signal) => {
|
|
56
|
+
const actor = (await hub.whoami({ signal })).actor
|
|
57
|
+
if (actor.kind !== "user") throw new CliError(EXIT.forbidden, "only the user can create a grant, not a session or app", GRANT_USER_HINT)
|
|
58
|
+
try {
|
|
59
|
+
return grantText((await hub.createGrant(spec(args) as unknown as GrantSpec, { signal })).grant)
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
throw grantError(cause)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// User-only MCP access to the existing console session-model route and owner-scoped machine/node snapshots.
|
|
2
|
+
// No agent or app model-setting authority: the server's model route remains console user-only.
|
|
3
|
+
import type { HubClient } from "../../sdk/src/client.ts"
|
|
4
|
+
import { CliError, EXIT } from "./http"
|
|
5
|
+
import { ToolArgError } from "./tool-args"
|
|
6
|
+
|
|
7
|
+
type Args = Record<string, unknown>
|
|
8
|
+
interface Tool {
|
|
9
|
+
readonly name: string
|
|
10
|
+
readonly description: string
|
|
11
|
+
readonly properties: Record<string, unknown>
|
|
12
|
+
readonly required?: readonly string[]
|
|
13
|
+
readonly readOnly: boolean
|
|
14
|
+
run(hub: HubClient, args: Args, signal: AbortSignal): Promise<string>
|
|
15
|
+
}
|
|
16
|
+
const STRING = { type: "string" } as const
|
|
17
|
+
const need = (args: Args, key: string) => {
|
|
18
|
+
const v = args[key]
|
|
19
|
+
if (typeof v !== "string" || !v.trim()) throw new ToolArgError(`"${key}" is required (a non-empty string)`)
|
|
20
|
+
return v
|
|
21
|
+
}
|
|
22
|
+
const userOnly = async (hub: HubClient, signal: AbortSignal) => {
|
|
23
|
+
if ((await hub.whoami({ signal })).actor.kind !== "user") throw new CliError(EXIT.forbidden, "only the user can use this tool", "Use the user's device credential")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const operationTools: readonly Tool[] = [
|
|
27
|
+
{
|
|
28
|
+
name: "session_model_set", description: "Set one existing session's model (providerID, modelID and optional variant), exactly as the console Model chip does. User device credential only; the console route checks ownership, catalog, availability and audit. An app or session cannot set it.",
|
|
29
|
+
properties: { session: STRING, model: { type: "object", properties: { providerID: STRING, modelID: STRING, variant: STRING }, required: ["providerID", "modelID"] } },
|
|
30
|
+
required: ["session", "model"], readOnly: false,
|
|
31
|
+
run: async (hub, args, signal) => {
|
|
32
|
+
await userOnly(hub, signal)
|
|
33
|
+
const model = args.model
|
|
34
|
+
if (typeof model !== "object" || model === null || Array.isArray(model)) throw new ToolArgError('"model" is required (providerID, modelID, variant?)')
|
|
35
|
+
const result = await hub.call<{ model: unknown }>("PUT", `/api/sessions/${encodeURIComponent(need(args, "session"))}/model`, { body: { model }, signal })
|
|
36
|
+
return JSON.stringify(result)
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "machines", description: "The user's own enrolled machines, online status and processes. User device credential only; wraps GET /api/machines. No machine tokens or secrets.",
|
|
41
|
+
properties: {}, readOnly: true,
|
|
42
|
+
run: async (hub, _args, signal) => { await userOnly(hub, signal); return JSON.stringify(await hub.call("GET", "/api/machines", { signal })) },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "launch_nodes", description: "The user's connected launch nodes: machine, workspace roots, capacity/used/free slots, runtimes (harnesses) and labels. User device credential only; no tokens, connection IDs or mutation.",
|
|
46
|
+
properties: {}, readOnly: true,
|
|
47
|
+
run: async (hub, _args, signal) => { await userOnly(hub, signal); return JSON.stringify(await hub.call("GET", "/api/agent/launch-nodes", { signal })) },
|
|
48
|
+
},
|
|
49
|
+
]
|
package/packages/cli/src/mcp.ts
CHANGED
|
@@ -25,8 +25,11 @@ import { launchedText, launchesText, launchText, stoppedLaunchText } from "./lau
|
|
|
25
25
|
import { artifactTools } from "./artifacts-mcp"
|
|
26
26
|
import { artifactJobTools } from "./artifact-jobs-mcp"
|
|
27
27
|
import { teamIntakeTools } from "./team-intake-mcp"
|
|
28
|
+
import { subscriptionTools } from "./subscriptions-mcp"
|
|
29
|
+
import { operationTools } from "./mcp-operations"
|
|
28
30
|
import { teamTools } from "./teams-mcp"
|
|
29
31
|
import { profileLifecycleTools } from "./profiles-mcp"
|
|
32
|
+
import { grantTools } from "./grants-mcp"
|
|
30
33
|
import { ToolArgError } from "./tool-args"
|
|
31
34
|
import { watchlistTools } from "./watchlist"
|
|
32
35
|
|
|
@@ -755,10 +758,13 @@ const TOOLS: readonly Tool[] = [
|
|
|
755
758
|
...webhookTools,
|
|
756
759
|
...profileTools,
|
|
757
760
|
...profileLifecycleTools,
|
|
761
|
+
...grantTools,
|
|
758
762
|
...workspaceTools,
|
|
759
763
|
...artifactTools,
|
|
760
764
|
...artifactJobTools,
|
|
761
765
|
...teamIntakeTools,
|
|
766
|
+
...subscriptionTools,
|
|
767
|
+
...operationTools,
|
|
762
768
|
...teamTools,
|
|
763
769
|
...watchlistTools,
|
|
764
770
|
{
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// MCP wrappers over the existing subscription routes; authorization and delivery caps remain on the server.
|
|
2
|
+
import type { HubClient } from "../../sdk/src/client.ts"
|
|
3
|
+
import type { SubscribeInput } from "../../sdk/src/types.ts"
|
|
4
|
+
import { subscribedText, subscriptionsText } from "./subscriptions"
|
|
5
|
+
import { ToolArgError } from "./tool-args"
|
|
6
|
+
|
|
7
|
+
type Args = Record<string, unknown>
|
|
8
|
+
export interface SubscriptionTool {
|
|
9
|
+
readonly name: string
|
|
10
|
+
readonly description: string
|
|
11
|
+
readonly properties: Record<string, unknown>
|
|
12
|
+
readonly required?: readonly string[]
|
|
13
|
+
readonly readOnly: boolean
|
|
14
|
+
readonly organizes?: boolean
|
|
15
|
+
run(hub: HubClient, args: Args, signal: AbortSignal): Promise<string>
|
|
16
|
+
}
|
|
17
|
+
const STRING = { type: "string" } as const
|
|
18
|
+
const need = (args: Args, key: string): string => {
|
|
19
|
+
const value = args[key]
|
|
20
|
+
if (typeof value !== "string" || !value.trim()) throw new ToolArgError(`"${key}" is required (a non-empty string)`)
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
const optional = (args: Args, key: string): string | undefined => {
|
|
24
|
+
const value = args[key]
|
|
25
|
+
if (value === undefined || value === null) return undefined
|
|
26
|
+
if (typeof value !== "string") throw new ToolArgError(`"${key}" must be a string`)
|
|
27
|
+
return value.trim() || undefined
|
|
28
|
+
}
|
|
29
|
+
const array = (args: Args, key: string): string[] | undefined => {
|
|
30
|
+
const value = args[key]
|
|
31
|
+
if (value === undefined || value === null) return undefined
|
|
32
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) throw new ToolArgError(`"${key}" must be a list of strings`)
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
const number = (args: Args, key: string, min: number, max: number): number | undefined => {
|
|
36
|
+
const value = args[key]
|
|
37
|
+
if (value === undefined || value === null) return undefined
|
|
38
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) throw new ToolArgError(`"${key}" must be a whole number from ${min} to ${max}`)
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const subscriptionTools: readonly SubscriptionTool[] = [
|
|
43
|
+
{
|
|
44
|
+
name: "subscribe", description: "Follow journal events for a subscriber session. A session subscribes itself; users and apps name subscriber. Apps need events:subscribe, sessions:read, relevant type scopes and messages:send unless silent. Hub checks visibility, rates and loops.",
|
|
45
|
+
properties: {
|
|
46
|
+
to: { type: ["string", "array"], items: STRING, description: "One or more session/role/team addresses, or children." },
|
|
47
|
+
subscriber: STRING, types: { type: "array", items: STRING }, delivery: { type: "string", enum: ["queue", "steer", "silent"] },
|
|
48
|
+
batch: { type: "number", description: "Seconds per batch (1–3600)." }, expires: { type: "number", description: "Seconds until expiry." },
|
|
49
|
+
maxPerHour: { type: "integer", minimum: 1, maximum: 120 }, note: STRING,
|
|
50
|
+
}, required: ["to"], readOnly: false,
|
|
51
|
+
run: async (hub, args, signal) => {
|
|
52
|
+
const to = typeof args.to === "string" && args.to.trim() ? args.to : array(args, "to")
|
|
53
|
+
if (!to || (Array.isArray(to) && !to.length)) throw new ToolArgError('"to" needs at least one address')
|
|
54
|
+
const delivery = optional(args, "delivery")
|
|
55
|
+
if (delivery && !["queue", "steer", "silent"].includes(delivery)) throw new ToolArgError('"delivery" is queue, steer or silent')
|
|
56
|
+
const subscriber = optional(args, "subscriber")
|
|
57
|
+
const types = array(args, "types")
|
|
58
|
+
const batch = number(args, "batch", 1, 3600)
|
|
59
|
+
const expires = number(args, "expires", 1, 30 * 86400)
|
|
60
|
+
const maxPerHour = number(args, "maxPerHour", 1, 120)
|
|
61
|
+
const note = optional(args, "note")
|
|
62
|
+
const input: SubscribeInput = { to, ...(subscriber ? { subscriber } : {}), ...(types ? { types } : {}), ...(delivery ? { delivery: delivery as SubscribeInput["delivery"] } : {}), ...(batch ? { batch } : {}), ...(expires ? { expires } : {}), ...(maxPerHour ? { maxPerHour } : {}), ...(note ? { note } : {}) }
|
|
63
|
+
return subscribedText(await hub.subscribe(input, { signal }))
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "subscriptions", description: "List subscriptions visible to this credential (session: its own; app: created by it; user: all). Optional session filter; route enforces scopes and tenancy.",
|
|
68
|
+
properties: { session: STRING }, readOnly: true,
|
|
69
|
+
run: async (hub, args, signal) => subscriptionsText(await hub.subscriptions({ ...(optional(args, "session") ? { session: optional(args, "session") } : {}), signal })),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "unsubscribe", description: "Remove a subscription this credential owns. An unknown or another caller's ID answers not_found.",
|
|
73
|
+
properties: { id: STRING }, required: ["id"], readOnly: false, organizes: true,
|
|
74
|
+
run: async (hub, args, signal) => { const id = need(args, "id"); await hub.unsubscribe(id, { signal }); return `unsubscribed ${id}` },
|
|
75
|
+
},
|
|
76
|
+
]
|
|
@@ -51,6 +51,7 @@ const SPEC_FIELDS = {
|
|
|
51
51
|
title: { ...STRING, description: "1–120 characters." },
|
|
52
52
|
description: STRING,
|
|
53
53
|
mode: { type: "string", enum: ["open", "restricted"], description: "open: every member may msg/ask every other member. restricted: only the drawn paths (edges)." },
|
|
54
|
+
hopLimit: { type: "integer", minimum: 1, maximum: 100, description: "Agent-only wake hop limit between this team's seats; may not exceed the hub-wide agents.hopLimit. Unset uses the global limit." },
|
|
54
55
|
roles: { type: "array", items: ROLE, description: "1–32 roles." },
|
|
55
56
|
entryRole: { ...STRING, description: "The role that receives submitted work (e.g. pm) and supervises members." },
|
|
56
57
|
edges: { type: "array", items: EDGE, description: "Communication paths between roles (restricted mode)." },
|
|
@@ -176,6 +177,7 @@ function specLines(spec: TeamSpec): string[] {
|
|
|
176
177
|
` ${r.name}${r.name === spec.entryRole ? " (entry)" : ""}${r.label ? ` "${one(r.label)}"` : ""}${r.externalContact ? " external contact" : ""}${r.minMembers !== undefined || r.maxMembers !== undefined ? ` members ${r.minMembers ?? 0}–${r.maxMembers ?? 16}` : ""}${r.profile ? ` profile ${r.profile.profileID}${r.profile.version ? `@${r.profile.version}` : ""}` : ""}`
|
|
177
178
|
return [
|
|
178
179
|
`mode: ${spec.mode}${spec.mode === "open" ? " (every member may msg/ask every other)" : " (only the paths below)"}`,
|
|
180
|
+
...(spec.hopLimit !== undefined ? [`hop limit: ${spec.hopLimit}`] : []),
|
|
179
181
|
`entry role: ${spec.entryRole}`,
|
|
180
182
|
"roles:",
|
|
181
183
|
...spec.roles.map(role),
|
|
@@ -196,7 +198,7 @@ function rosterLines(roster: TeamRoster): string[] {
|
|
|
196
198
|
const flags = [r.vacant ? "vacant" : "", r.understaffed ? `understaffed (min ${r.minMembers})` : "", r.launching ? `${r.launching} launching` : ""].filter(Boolean)
|
|
197
199
|
lines.push(` ${r.role}${r.entry ? " (entry)" : ""} ${r.seats.length}/${r.maxMembers} ${r.online} online${flags.length ? ` ${flags.join(", ")}` : ""}`)
|
|
198
200
|
for (const s of r.seats)
|
|
199
|
-
lines.push(` ${s.staffing.id} ${s.staffing.sessionID} ${s.presence} gen ${s.staffing.generation} r${s.staffing.revision}${s.profile ? ` profile ${s.profile.status}${s.profile.matches ? "" : " (mismatch)"}` : ""}`)
|
|
201
|
+
lines.push(` ${s.staffing.id} ${s.staffing.sessionID} ${s.presence}${s.stalled ? ` stalled: ${s.stalled}` : ""} gen ${s.staffing.generation} r${s.staffing.revision}${s.profile ? ` profile ${s.profile.status}${s.profile.matches ? "" : " (mismatch)"}` : ""}`)
|
|
200
202
|
}
|
|
201
203
|
const failed = roster.requests.filter((q) => q.state !== "staffed").slice(-5)
|
|
202
204
|
for (const q of failed) lines.push(` launch ${q.id} ${q.role} ${q.state}${q.reason ? `: ${one(q.reason)}` : ""}`)
|
|
@@ -347,7 +349,7 @@ export const teamTools: readonly TeamTool[] = [
|
|
|
347
349
|
need(args, "name")
|
|
348
350
|
need(args, "title")
|
|
349
351
|
const key = optional(args, "idempotencyKey")
|
|
350
|
-
const spec = picked(args, ["name", "title", "description", "mode", "roles", "entryRole", "edges", "tags", "metadata", "projects"]) as unknown as TeamSpec
|
|
352
|
+
const spec = picked(args, ["name", "title", "description", "mode", "hopLimit", "roles", "entryRole", "edges", "tags", "metadata", "projects"]) as unknown as TeamSpec
|
|
351
353
|
const { team, created } = await hub.teams.create({ ...spec, edges: spec.edges ?? [] }, { signal, ...(key ? { idempotencyKey: key } : {}) })
|
|
352
354
|
return [`${created ? "created" : "already created (same key)"}: ${teamLine(team)}`, ...specLines(team)].join("\n")
|
|
353
355
|
},
|
|
@@ -362,7 +364,7 @@ export const teamTools: readonly TeamTool[] = [
|
|
|
362
364
|
idempotent: true,
|
|
363
365
|
run: async (hub, args, signal) => {
|
|
364
366
|
const expectedRevision = revision(args)
|
|
365
|
-
const patch = picked(args, ["title", "description", "mode", "roles", "entryRole", "edges", "tags", "metadata", "projects"]) as TeamPatch
|
|
367
|
+
const patch = picked(args, ["title", "description", "mode", "hopLimit", "roles", "entryRole", "edges", "tags", "metadata", "projects"]) as TeamPatch
|
|
366
368
|
if (Object.keys(patch).length === 0) throw new ArgError("give at least one field to change")
|
|
367
369
|
const id = need(args, "team")
|
|
368
370
|
const { team } = await hub.teams.update(id, patch, expectedRevision, { signal })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-codex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "The Codex app-server spoke: attaches to Codex's app-server (its own over stdio, and the shared daemon's socket when present), and the `hub codex` wrapper (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "`hub node`: the per-machine daemon that prepares, collects and cleans up launch workspaces (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-preset",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "The preset spoke: drives a harness without a spoke of its own through a terminal pane (tmux), described by a data-only preset; official presets in presets/ (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,7 @@ export const TEAM_LIMITS = {
|
|
|
19
19
|
sessionsPerRole: 16,
|
|
20
20
|
/** Open intake items per team. */
|
|
21
21
|
openIntakePerTeam: 500,
|
|
22
|
+
hopLimit: 100,
|
|
22
23
|
} as const
|
|
23
24
|
|
|
24
25
|
export const TEAM_SCOPES = ["teams:read", "teams:manage", "teams:submit"] as const
|
|
@@ -54,6 +55,8 @@ export interface TeamDefinition extends Revisioned {
|
|
|
54
55
|
readonly description?: string
|
|
55
56
|
readonly state: RecordState
|
|
56
57
|
readonly mode: TeamMode
|
|
58
|
+
/** Optional limit for messages between this team's seated sessions. */
|
|
59
|
+
readonly hopLimit?: number
|
|
57
60
|
readonly roles: readonly TeamRole[]
|
|
58
61
|
/** The intake role; receives submitted work and supervises members (via workspace supervision). */
|
|
59
62
|
readonly entryRole: string
|
|
@@ -182,6 +185,7 @@ export function teamInputProblem(v: unknown): string | undefined {
|
|
|
182
185
|
if (!isLabel(v.title)) return "title must be 1–120 characters of text"
|
|
183
186
|
if (v.description !== undefined && !isText(v.description, DESCRIPTION_MAX)) return `description must be at most ${DESCRIPTION_MAX} characters`
|
|
184
187
|
if (v.mode !== "open" && v.mode !== "restricted") return "mode must be open or restricted"
|
|
188
|
+
if (v.hopLimit !== undefined && (!Number.isSafeInteger(v.hopLimit) || (v.hopLimit as number) < 1 || (v.hopLimit as number) > TEAM_LIMITS.hopLimit)) return `hopLimit must be a whole number from 1 to ${TEAM_LIMITS.hopLimit}`
|
|
185
189
|
if (!Array.isArray(v.roles) || v.roles.length === 0 || v.roles.length > TEAM_LIMITS.rolesPerTeam) return `roles must list 1–${TEAM_LIMITS.rolesPerTeam} roles`
|
|
186
190
|
const names = new Set<string>()
|
|
187
191
|
for (const role of v.roles) {
|
|
@@ -2,7 +2,8 @@ export const RECORDING_FORMAT = "hub-voice-recording" as const
|
|
|
2
2
|
export const RECORDING_FORMAT_VERSION = 1
|
|
3
3
|
export const RECORDING_AUDIO_TYPE = "audio/wav" // 16 kHz mono PCM16, exactly as sent to STT
|
|
4
4
|
export const RECORDING_PAGE_TYPE = "application/x-ndjson" // event pages (blobs)
|
|
5
|
-
export const
|
|
5
|
+
export const RECORDING_REPLY_TYPE = "audio/mpeg" // Developer mode: a reply sentence's TTS audio, as synthesized
|
|
6
|
+
export const RECORDING_MEDIA_TYPES = [RECORDING_AUDIO_TYPE, RECORDING_PAGE_TYPE, RECORDING_REPLY_TYPE] as const
|
|
6
7
|
export const RECORDING_HEADER = "x-hub-recording" // on /api/transcribe and /api/speak: the recording ID
|
|
7
8
|
export const RECORDING_SEGMENT_HEADER = "x-hub-segment" // on /api/transcribe: the segment number
|
|
8
9
|
export const RECORDING_REF_HEADER = "x-hub-tts-ref" // on /api/speak: "<messageID>:<ordinal>:<seq>:<i>"
|
|
@@ -11,8 +12,12 @@ export const RECORDING_AUDIO_RESULT_HEADER = "x-hub-recording-audio" // transcri
|
|
|
11
12
|
* Console kinds and values added after version 1, listed in the start answer (`features`): a console sends them only
|
|
12
13
|
* to a hub that lists them (an older hub refuses a whole batch with an unknown kind or value).
|
|
13
14
|
* "turn.predict": the turn.predict kind and utterance.sent reasons "smart" / "smart-fallback" (Smart Turn).
|
|
15
|
+
* "capture.v2": the kinds vad.near, vad.summary, stt.slow and mark, the utterance.command command "mark", and the
|
|
16
|
+
* stt.received fields junkReason, held and speechMs (an older hub refuses an unknown field too).
|
|
17
|
+
* "dev.v1": Developer mode's kinds vad.trace, audio.clip, device.info, resource.timing and console.log (sent only
|
|
18
|
+
* when the start answer also says `developer: true`), and the clip upload route.
|
|
14
19
|
*/
|
|
15
|
-
export const RECORDING_FEATURES = ["turn.predict"] as const
|
|
20
|
+
export const RECORDING_FEATURES = ["turn.predict", "capture.v2", "dev.v1"] as const
|
|
16
21
|
export type RecordingFeature = (typeof RECORDING_FEATURES)[number]
|
|
17
22
|
|
|
18
23
|
export const RECORDING_LIMITS = {
|
|
@@ -36,8 +41,29 @@ export const RECORDING_LIMITS = {
|
|
|
36
41
|
hubFlushMs: 2_000, hubFlushEvents: 100,
|
|
37
42
|
} as const
|
|
38
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Developer mode (feature "dev.v1"): the extras' own limits. Their audio (clips, reply audio) counts against the same
|
|
46
|
+
* caps but stops first: never within the headroom kept for segments below the user's and the recording's caps.
|
|
47
|
+
*/
|
|
48
|
+
export const DEV_LIMITS = {
|
|
49
|
+
clipsPerRecording: 200,
|
|
50
|
+
clipPreMs: 3000,
|
|
51
|
+
clipPostMs: 2000,
|
|
52
|
+
clipBytes: 192 * 1024, // 5 s at 16 kHz PCM16 = 160,044 B
|
|
53
|
+
/** At most one clip per this much time (console), the rest are not cut. */
|
|
54
|
+
clipGapMs: 2000,
|
|
55
|
+
repliesPerRecording: 2000,
|
|
56
|
+
replyBytes: 1024 * 1024, // one sentence's MP3
|
|
57
|
+
userHeadroomBytes: 50 * 1024 * 1024, // extras stop at userBytes − this; segments go on to userBytes
|
|
58
|
+
recordingHeadroomBytes: 15 * 1024 * 1024,
|
|
59
|
+
tracesPerMinute: 30,
|
|
60
|
+
logsPerMinute: 20,
|
|
61
|
+
} as const
|
|
62
|
+
|
|
39
63
|
export interface RecordingSettings {
|
|
40
64
|
readonly enabled: boolean // default false
|
|
65
|
+
/** Developer mode (implies enabled; turning recording off turns it off). Absent: off. */
|
|
66
|
+
readonly developer?: boolean
|
|
41
67
|
readonly revision: number
|
|
42
68
|
readonly updatedAt?: number
|
|
43
69
|
}
|
|
@@ -75,6 +101,11 @@ export interface RecordingSummary {
|
|
|
75
101
|
readonly events: number
|
|
76
102
|
readonly audio: boolean // false once a cap stopped audio
|
|
77
103
|
readonly redactions: number
|
|
104
|
+
/** Developer mode: this recording keeps the extras (until Developer mode is turned off). */
|
|
105
|
+
readonly developer?: true
|
|
106
|
+
/** Developer mode: audio clips and reply audio stored. */
|
|
107
|
+
readonly clips?: number
|
|
108
|
+
readonly replies?: number
|
|
78
109
|
}
|
|
79
110
|
|
|
80
111
|
export interface RecordingUsage { readonly bytes: number; readonly recordings: number; readonly capBytes: number }
|
|
@@ -90,11 +121,17 @@ export type ConsoleEventKind =
|
|
|
90
121
|
| "tts.sentence.ended" | "barge.in" | "cue.played" | "mic" | "control" | "turn.timing" | "decision.voice"
|
|
91
122
|
| "clock.sync" | "error"
|
|
92
123
|
| "turn.predict" // feature "turn.predict"
|
|
124
|
+
| "vad.near" | "vad.summary" | "stt.slow" | "mark" // feature "capture.v2"
|
|
125
|
+
| "vad.trace" | "audio.clip" | "device.info" | "resource.timing" | "console.log" // feature "dev.v1" (Developer mode)
|
|
93
126
|
export type HubEventKind =
|
|
94
127
|
| "recording.started" | "recording.ended" | "conversation" | "stt.request" | "stt.result" | "tts.synth"
|
|
95
128
|
| "utterance.received" | "utterance.delivered" | "turn.started" | "first.token" | "say.sent" | "reply.full"
|
|
96
129
|
| "turn.ended" | "tool" | "interrupt" | "wake.arrived" | "concierge.tool" | "decision.opened" | "decision.answered"
|
|
97
130
|
| "cap.reached" | "error"
|
|
131
|
+
| "turn.detail" | "request" | "reply.audio" // Developer mode
|
|
132
|
+
|
|
133
|
+
/** Console kinds of Developer mode: stored only in a developer recording. */
|
|
134
|
+
export const DEV_CONSOLE_KINDS: readonly ConsoleEventKind[] = ["vad.trace", "audio.clip", "device.info", "resource.timing", "console.log"]
|
|
98
135
|
|
|
99
136
|
/** As stored and exported (events.jsonl). */
|
|
100
137
|
export interface RecordingEvent {
|
|
@@ -29,7 +29,9 @@ poll="${HUB_POLL_INTERVAL:-0.5}"
|
|
|
29
29
|
say() { echo "[hub-sandbox] $*" >&2; }
|
|
30
30
|
|
|
31
31
|
mkdir -p "$hub"
|
|
32
|
-
|
|
32
|
+
pid_file=$(mktemp "$hub/entry.pid.XXXXXX") || exit 1
|
|
33
|
+
printf '%s\n' "$$" >"$pid_file"
|
|
34
|
+
mv -f "$pid_file" "$hub/entry.pid"
|
|
33
35
|
rm -f "$hub/exited"
|
|
34
36
|
|
|
35
37
|
# 1. Wait for the start op.
|
|
@@ -20,5 +20,7 @@ else
|
|
|
20
20
|
nohup "$here/hub-sandbox-entry" >>"$log" 2>&1 </dev/null &
|
|
21
21
|
fi
|
|
22
22
|
# The entry rewrites this with its own pid; written here too so a start right after sees it.
|
|
23
|
-
|
|
23
|
+
pid_file=$(mktemp "$hub/entry.pid.XXXXXX") || exit 1
|
|
24
|
+
printf '%s\n' "$!" >"$pid_file"
|
|
25
|
+
mv -f "$pid_file" "$hub/entry.pid"
|
|
24
26
|
echo "hub-sandbox: started (pid $!, log $log)"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-runtime-openshell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "The `openshell` runtime of `hub node`: launches in NVIDIA OpenShell sandboxes, driven through the openshell CLI (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -780,6 +780,19 @@ export class HubClient {
|
|
|
780
780
|
return this.call("GET", `${AGENT}/grants`, signalOf(options))
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
+
/** Grants held by this session/app; a user credential has no holder. */
|
|
784
|
+
heldGrants(options: CallOptions = {}): Promise<{ grants: Grant[] }> {
|
|
785
|
+
return this.call("GET", `${AGENT}/grants/mine`, signalOf(options))
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
heldGrant(id: string, options: CallOptions = {}): Promise<{ grant: Grant }> {
|
|
789
|
+
return this.call("GET", `${AGENT}/grants/mine/${segment(id)}`, signalOf(options))
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
revokeHeldGrant(id: string, options: CallOptions = {}): Promise<{ grant: Grant }> {
|
|
793
|
+
return this.call("DELETE", `${AGENT}/grants/mine/${segment(id)}`, signalOf(options))
|
|
794
|
+
}
|
|
795
|
+
|
|
783
796
|
grant(id: string, options: CallOptions = {}): Promise<{ grant: Grant }> {
|
|
784
797
|
return this.call("GET", `${AGENT}/grants/${segment(id)}`, signalOf(options))
|
|
785
798
|
}
|
|
@@ -53,6 +53,7 @@ export interface TeamSpec {
|
|
|
53
53
|
readonly title: string
|
|
54
54
|
readonly description?: string
|
|
55
55
|
readonly mode: TeamMode
|
|
56
|
+
readonly hopLimit?: number
|
|
56
57
|
readonly roles: readonly TeamRole[]
|
|
57
58
|
/** The intake role (e.g. the project manager): receives submitted work and supervises members. */
|
|
58
59
|
readonly entryRole: string
|
|
@@ -149,6 +150,7 @@ export interface TeamWorkspaceLink {
|
|
|
149
150
|
export interface TeamRosterSeat {
|
|
150
151
|
readonly staffing: TeamStaffing
|
|
151
152
|
readonly presence: TeamSessionPresence
|
|
153
|
+
readonly stalled?: "hop cap"
|
|
152
154
|
readonly memberID?: string
|
|
153
155
|
readonly supervisorMemberID?: string
|
|
154
156
|
readonly profile?: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-spoke-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Harness-neutral spoke building blocks: hub connection, enrollment, tokens, idempotent deliver, prompt references, HarnessSpoke (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -124,7 +124,13 @@ export function appAnswerRefusal(decision: Pick<Decision, "tier" | "action" | "a
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** What answering a Decision does besides replying: an escalated permission request (P3-T8) or a grant request (P3-T9). */
|
|
127
|
-
export type DecisionAction = PermissionAction | GrantRequestAction | ActAction
|
|
127
|
+
export type DecisionAction = PermissionAction | GrantRequestAction | ActAction | TeamHopRenewAction
|
|
128
|
+
|
|
129
|
+
/** Hub-created, tap-only action: an agent cannot authorize a team's hop budget renewal. */
|
|
130
|
+
export interface TeamHopRenewAction {
|
|
131
|
+
readonly kind: "teamHopRenew"
|
|
132
|
+
readonly teamID: string
|
|
133
|
+
}
|
|
128
134
|
|
|
129
135
|
/**
|
|
130
136
|
* (CON-06) A Concierge proposal (concierge/proposals.ts): answering `go` runs the act verb on the target AS THE USER
|