@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
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
// - close(): a Decision whose subject went away (a permission answered elsewhere) is closed, not answered; the asker
|
|
21
21
|
// hears why.
|
|
22
22
|
// - Every open and change (answer, timeout, escalation, expiry, close) goes to `changed` (the console's 'decision' event).
|
|
23
|
+
// - (CON-06) `via`: an answer is a tap unless it says voice or text. Voice/text answers only a confirm-tier
|
|
24
|
+
// Decision, and only when the hub's `via` check admits it (the Concierge tiers: armed, presence, the user's
|
|
25
|
+
// device); otherwise forbidden "Answer this decision with a tap". Never a model: only the user's device answers.
|
|
26
|
+
// - (CON-06) `as: "concierge"`: set when the asker is one of the user's Concierge sessions (current or past), so
|
|
27
|
+
// the console can say "by @@concierge". The `act` action kind is a Concierge proposal (concierge/proposals.ts).
|
|
23
28
|
import type { Clock, StateStore, Timers } from "../backend/types"
|
|
24
29
|
import type { AgentActor, UserActor } from "./actor"
|
|
25
30
|
import type { Delivery } from "./delivery"
|
|
@@ -37,6 +42,22 @@ export type DecisionState = "open" | "answered" | "expired" | "closed"
|
|
|
37
42
|
export const URGENCIES: readonly Urgency[] = ["low", "normal", "high"]
|
|
38
43
|
export const TIERS: readonly Tier[] = ["confirm", "tap"]
|
|
39
44
|
export const DECISION_STATES: readonly DecisionState[] = ["open", "answered", "expired", "closed"]
|
|
45
|
+
/** (CON-06) How the user answered: tap (the default, absent), or voice/text in the Concierge panel. */
|
|
46
|
+
export type AnswerVia = "tap" | "voice" | "text"
|
|
47
|
+
export const ANSWER_VIAS: readonly AnswerVia[] = ["tap", "voice", "text"]
|
|
48
|
+
/** The refusal of a voice/text answer the hub does not admit. */
|
|
49
|
+
export const TAP_ONLY = "Answer this decision with a tap"
|
|
50
|
+
/** The refusal of a voice/text answer that picks a lasting option (coordinator decision, 93265or7). */
|
|
51
|
+
export const ALWAYS_TAP_ONLY = "Answer 'always' with a tap"
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether an option is not once-off: `always` (saves a lasting rule, e.g. an escalated permission approved for
|
|
55
|
+
* good), or approving a grant request (creates a lasting grant). Voice and text pick only once-off options.
|
|
56
|
+
*/
|
|
57
|
+
export function lastingOption(decision: Pick<Decision, "action" | "options">, option: string | undefined): boolean {
|
|
58
|
+
if (option === "always") return true
|
|
59
|
+
return decision.action?.kind === "grant" && !decision.options.some((o) => o.id === option && o.safe)
|
|
60
|
+
}
|
|
40
61
|
|
|
41
62
|
/** Options a Decision offers. */
|
|
42
63
|
export const OPTIONS_MAX = 8
|
|
@@ -67,10 +88,27 @@ export interface DecisionAnswer {
|
|
|
67
88
|
readonly at: number
|
|
68
89
|
/** The device the user answered from. */
|
|
69
90
|
readonly deviceID?: string
|
|
91
|
+
/** (CON-06) voice or text; absent = tap. */
|
|
92
|
+
readonly via?: "voice" | "text"
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
/** What answering a Decision does besides replying: an escalated permission request (P3-T8) or a grant request (P3-T9). */
|
|
73
|
-
export type DecisionAction = PermissionAction | GrantRequestAction
|
|
96
|
+
export type DecisionAction = PermissionAction | GrantRequestAction | ActAction
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* (CON-06) A Concierge proposal (concierge/proposals.ts): answering `go` runs the act verb on the target AS THE USER
|
|
100
|
+
* through the acting engine (tenancy re-checked); `hold` does nothing.
|
|
101
|
+
*/
|
|
102
|
+
export interface ActAction {
|
|
103
|
+
readonly kind: "act"
|
|
104
|
+
readonly verb: "stop" | "deny" | "prompt" | "answer"
|
|
105
|
+
/** The session ID it was proposed for (resolved when proposed). */
|
|
106
|
+
readonly target: string
|
|
107
|
+
/** The verb's validated input (requestID, formID, answer, text). */
|
|
108
|
+
readonly input: Readonly<Record<string, unknown>>
|
|
109
|
+
/** (FX-05) deny: the permission it would deny, for the console's card. */
|
|
110
|
+
readonly request?: DecisionRequest
|
|
111
|
+
}
|
|
74
112
|
|
|
75
113
|
/** An escalated permission request: the answer replies to it as the user. */
|
|
76
114
|
export interface PermissionAction {
|
|
@@ -82,6 +120,56 @@ export interface PermissionAction {
|
|
|
82
120
|
readonly sessionID: string
|
|
83
121
|
/** The agent asked to approve it always. */
|
|
84
122
|
readonly always: boolean
|
|
123
|
+
/** (FX-05) What it asks, for the console's card (absent when the hub did not know the request's details). */
|
|
124
|
+
readonly request?: DecisionRequest
|
|
125
|
+
/** (FX-05) Why the hub sent it to the user (the evaluation's reason). */
|
|
126
|
+
readonly reason?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* (FX-05) A permission request as a Decision shows it (bounded): its kind, what it applies to and the patterns an
|
|
131
|
+
* "always" would save. What was not stored is recorded, so a shortened command never reads as complete: the totals
|
|
132
|
+
* when items were dropped, each cut item with its full length, and `truncated` when the hub received the request
|
|
133
|
+
* already shortened (src/status.ts permissionRequest). The console renders it as text; the Concierge's Decision reads
|
|
134
|
+
* leave it out (concierge/board-routes.ts conciergeDecisionView).
|
|
135
|
+
*/
|
|
136
|
+
export interface DecisionRequest {
|
|
137
|
+
readonly action: string
|
|
138
|
+
readonly resources: readonly string[]
|
|
139
|
+
readonly save?: readonly string[]
|
|
140
|
+
/** How many resources the request had, when more than were stored. */
|
|
141
|
+
readonly resourcesTotal?: number
|
|
142
|
+
/** How many save patterns it had, when more than were stored. */
|
|
143
|
+
readonly savesTotal?: number
|
|
144
|
+
/** Stored items cut at DECISION_REQUEST_TEXT characters, with their full length. */
|
|
145
|
+
readonly cut?: readonly { readonly field: "resources" | "save"; readonly index: number; readonly length: number }[]
|
|
146
|
+
/** The hub received the request already shortened. */
|
|
147
|
+
readonly truncated?: true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Resources (and save patterns) a Decision keeps of a request, and the characters of each. */
|
|
151
|
+
export const DECISION_REQUEST_ITEMS = 20
|
|
152
|
+
export const DECISION_REQUEST_TEXT = 2_000
|
|
153
|
+
|
|
154
|
+
/** A permission request, bounded for a Decision, with what was not stored. */
|
|
155
|
+
export function decisionRequest(request: { readonly action: string; readonly resources: readonly string[]; readonly save?: readonly string[]; readonly truncated?: true }): DecisionRequest {
|
|
156
|
+
const cut: { field: "resources" | "save"; index: number; length: number }[] = []
|
|
157
|
+
const bound = (field: "resources" | "save", list: readonly string[]) =>
|
|
158
|
+
list.slice(0, DECISION_REQUEST_ITEMS).map((r, index) => {
|
|
159
|
+
if (r.length > DECISION_REQUEST_TEXT) cut.push({ field, index, length: r.length })
|
|
160
|
+
return r.slice(0, DECISION_REQUEST_TEXT)
|
|
161
|
+
})
|
|
162
|
+
const resources = bound("resources", request.resources)
|
|
163
|
+
const save = request.save?.length ? bound("save", request.save) : undefined
|
|
164
|
+
return {
|
|
165
|
+
action: request.action.slice(0, DECISION_REQUEST_TEXT),
|
|
166
|
+
resources,
|
|
167
|
+
...(save ? { save } : {}),
|
|
168
|
+
...(request.resources.length > resources.length ? { resourcesTotal: request.resources.length } : {}),
|
|
169
|
+
...(save && request.save!.length > save.length ? { savesTotal: request.save!.length } : {}),
|
|
170
|
+
...(cut.length ? { cut } : {}),
|
|
171
|
+
...(request.truncated ? { truncated: true as const } : {}),
|
|
172
|
+
}
|
|
85
173
|
}
|
|
86
174
|
|
|
87
175
|
/** A grant an agent requested (grant-requests.ts): approving creates it as the user. */
|
|
@@ -121,6 +209,8 @@ export interface Decision {
|
|
|
121
209
|
readonly closed?: { readonly at: number; readonly reason: string }
|
|
122
210
|
/** Run (as the answering user) when the user answers; set by the hub only, never by an asker. */
|
|
123
211
|
readonly action?: DecisionAction
|
|
212
|
+
/** (CON-06) Asked by one of the user's Concierge sessions: the console says "by @@concierge". */
|
|
213
|
+
readonly as?: "concierge"
|
|
124
214
|
}
|
|
125
215
|
|
|
126
216
|
/** What the hub (never an asker's request) adds to an ask. */
|
|
@@ -148,8 +238,13 @@ export interface AskInput {
|
|
|
148
238
|
export interface AnswerInput {
|
|
149
239
|
readonly option?: string
|
|
150
240
|
readonly text?: string
|
|
241
|
+
/** (CON-06) Absent = tap. */
|
|
242
|
+
readonly via?: AnswerVia
|
|
151
243
|
}
|
|
152
244
|
|
|
245
|
+
/** (CON-06) Admits a voice/text answer (throws forbidden otherwise). */
|
|
246
|
+
export type ViaCheck = (userID: string, decision: Decision, via: "voice" | "text", by: UserActor) => Promise<void>
|
|
247
|
+
|
|
153
248
|
export type NotifyReason = "opened" | "escalated"
|
|
154
249
|
|
|
155
250
|
/**
|
|
@@ -177,6 +272,10 @@ export interface DecisionsDeps {
|
|
|
177
272
|
readonly changed?: (userID: string, decision: Decision) => void
|
|
178
273
|
/** Runs Decisions' actions (escalations); a Decision with an action cannot be answered without it. */
|
|
179
274
|
readonly act?: DecisionActor
|
|
275
|
+
/** (CON-06) Admits voice/text answers; without it every voice/text answer is refused. */
|
|
276
|
+
readonly via?: ViaCheck
|
|
277
|
+
/** (CON-06) Whether a session is one of the user's Concierge sessions (current or past): the Decision's `as`. */
|
|
278
|
+
readonly concierge?: (userID: string, sessionID: string) => Promise<boolean>
|
|
180
279
|
}
|
|
181
280
|
|
|
182
281
|
const DECISIONS = "decisions"
|
|
@@ -274,6 +373,7 @@ export class Decisions {
|
|
|
274
373
|
const v = validate(input)
|
|
275
374
|
const about = [...new Set(input.about ?? (actor.kind === "session" ? [actor.sessionID] : []))]
|
|
276
375
|
const { message } = await this.deps.delivery.send(userID, actor, "@me", "ask", input.question, { expiresInMs: v.expiresInMs })
|
|
376
|
+
const asConcierge = actor.kind === "session" && (await this.isConcierge(userID, actor.sessionID))
|
|
277
377
|
const decision: Decision = {
|
|
278
378
|
id: this.newID(),
|
|
279
379
|
messageID: message.id,
|
|
@@ -291,6 +391,7 @@ export class Decisions {
|
|
|
291
391
|
at: message.at,
|
|
292
392
|
expiresAt: message.expiresAt ?? message.at + v.expiresInMs,
|
|
293
393
|
...(extra.action ? { action: extra.action } : {}),
|
|
394
|
+
...(asConcierge ? { as: "concierge" as const } : {}),
|
|
294
395
|
}
|
|
295
396
|
const written = await this.deps.state.put(userID, DECISIONS, decision.id, decision, { ifVersion: 0 })
|
|
296
397
|
if (!written.ok) throw new Error("decision id collision")
|
|
@@ -331,9 +432,18 @@ export class Decisions {
|
|
|
331
432
|
if (!current || by.userID !== userID) throw notFound()
|
|
332
433
|
if (current.state !== "open") throw gone()
|
|
333
434
|
if (current.tier === "tap" && by.via !== "cookie") throw new AgentError("forbidden", "This decision needs a tap in the signed-in console", { hint: "Answer it in the console" })
|
|
435
|
+
const via = input.via ?? "tap"
|
|
436
|
+
if (!ANSWER_VIAS.includes(via)) throw invalid(`via is one of ${ANSWER_VIAS.join(", ")}`)
|
|
334
437
|
const choice = this.choice(current, input)
|
|
438
|
+
if (via !== "tap") {
|
|
439
|
+
// Voice/text: confirm tier only, and only what the hub's check admits (never without one).
|
|
440
|
+
if (current.tier !== "confirm" || !this.deps.via) throw new AgentError("forbidden", TAP_ONLY, { hint: "Tap an option" })
|
|
441
|
+
// Only once-off options by voice or text: a lasting one (always, a grant) needs a tap.
|
|
442
|
+
if (lastingOption(current, choice.option)) throw new AgentError("forbidden", ALWAYS_TAP_ONLY, { hint: "Tap the option" })
|
|
443
|
+
await this.deps.via(userID, current, via, by)
|
|
444
|
+
}
|
|
335
445
|
const at = this.now()
|
|
336
|
-
const answer: DecisionAnswer = { ...choice, answeredBy: "user", at, deviceID: by.deviceID }
|
|
446
|
+
const answer: DecisionAnswer = { ...choice, answeredBy: "user", at, deviceID: by.deviceID, ...(via !== "tap" ? { via } : {}) }
|
|
337
447
|
const decision = await this.settle(userID, id, answer)
|
|
338
448
|
if (!decision) throw gone()
|
|
339
449
|
const note = decision.action ? await this.runAction(userID, decision, choice, by) : undefined
|
|
@@ -399,6 +509,19 @@ export class Decisions {
|
|
|
399
509
|
|
|
400
510
|
// ---- internals ----------------------------------------------------------------------------------------
|
|
401
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Whether the session is one of the user's Concierge sessions; a failed read is no label (never fails the ask).
|
|
514
|
+
* (FX-07) A read that timed out (`busy`) refuses the ask instead: the label could not be checked (fail closed).
|
|
515
|
+
*/
|
|
516
|
+
private async isConcierge(userID: string, sessionID: string): Promise<boolean> {
|
|
517
|
+
try {
|
|
518
|
+
return (await this.deps.concierge?.(userID, sessionID)) === true
|
|
519
|
+
} catch (cause) {
|
|
520
|
+
if (cause instanceof AgentError && cause.code === "busy") throw cause
|
|
521
|
+
return false
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
402
525
|
/** Tells the changed listener (its failure never fails the caller). */
|
|
403
526
|
private emit(userID: string, decision: Decision): void {
|
|
404
527
|
try {
|