@huynle/hub 0.8.7 → 0.9.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 +129 -0
- package/README.md +69 -12
- package/package.json +5 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +278 -35
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/cli.ts +23 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/launches.ts +5 -1
- package/packages/cli/src/mcp.ts +28 -8
- package/packages/cli/src/messaging.ts +14 -0
- package/packages/cli/src/team-intake-mcp.ts +26 -2
- 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/schema/app-delivery/v1.schema.json +677 -0
- package/packages/protocol/src/artifacts.ts +43 -0
- package/packages/protocol/src/credentials.ts +38 -0
- package/packages/protocol/src/index.ts +56 -5
- package/packages/protocol/src/launch.ts +7 -2
- package/packages/protocol/src/markdown.ts +753 -0
- package/packages/protocol/src/media-allowlist.ts +240 -0
- package/packages/protocol/src/media.ts +120 -0
- package/packages/protocol/src/preview-bundle.ts +221 -0
- package/packages/protocol/src/voice-recording.ts +8 -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/client.ts +17 -3
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/team-intake.ts +8 -0
- package/packages/sdk/src/types.ts +16 -2
- package/packages/spoke-sdk/package.json +1 -1
- package/src/central/agent/delivery.ts +35 -6
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/events.ts +11 -2
- package/src/central/agent/launch-engine.ts +65 -4
- package/src/central/agent/launch-routes.ts +8 -0
- package/src/central/agent/launch-spec.ts +18 -1
- package/src/central/agent/launch-store.ts +2 -2
- package/src/central/agent/message-routes.ts +49 -11
- package/src/central/agent/messages.ts +17 -4
- package/src/central/agent/not-after.ts +28 -0
- package/src/central/agent/placement.ts +36 -4
- package/src/central/agent/routes.ts +3 -1
- package/src/central/agent/webhook-routes.ts +21 -9
- package/src/central/agent/webhooks.ts +27 -9
- package/src/central/artifacts/host.ts +3 -1
- package/src/central/artifacts/maintenance.ts +2 -1
- package/src/central/artifacts/preview-bundle.ts +81 -0
- package/src/central/artifacts/previews.ts +94 -0
- package/src/central/artifacts/routes.ts +12 -1
- package/src/central/artifacts/store.ts +198 -8
- package/src/central/config.ts +34 -0
- package/src/central/fetch-budget.ts +59 -0
- package/src/central/fetcher.ts +372 -0
- package/src/central/http.ts +277 -22
- package/src/central/media-remote.ts +248 -0
- package/src/central/media-timeline.ts +317 -0
- package/src/central/media-unfurl.ts +119 -0
- package/src/central/profiles/content.ts +3 -35
- package/src/central/recording/routes.ts +2 -2
- package/src/central/recording/schema.ts +3 -1
- package/src/central/registry.ts +449 -21
- package/src/central/spoke-ws.ts +17 -3
- package/src/central/store.ts +21 -0
- package/src/central/teams/intake-routes.ts +2 -1
- package/src/central/teams/intake.ts +200 -15
- package/src/central/unfurl-parse.ts +313 -0
- package/src/index.ts +68 -8
- package/src/speakable.ts +33 -1
- package/src/spoke/conversation.ts +7 -4
- package/src/spoke/deliver.ts +19 -0
- package/src/spoke/media-bytes.ts +171 -0
- package/src/spoke/media-policy.ts +394 -0
- package/src/spoke/media.ts +189 -0
- package/src/spoke/own-server.ts +30 -0
- package/src/spoke/preview-bundle.ts +538 -0
- package/src/spoke/spoke.ts +387 -17
- package/src/spoke/timeline.ts +59 -5
- package/src/spoke/voice-front.ts +335 -0
- package/web/dist/assets/24BXRY42-Zfr7jm_p.js +2 -0
- package/web/dist/assets/4FHPYW2L-C6Z67098.js +2 -0
- package/web/dist/assets/4LWW3ZNH-CUiYeygH.js +1 -0
- package/web/dist/assets/5THPTBWS-Ce_hS451.js +1 -0
- package/web/dist/assets/5XYWFMKW-CW-BCKiK.js +4 -0
- package/web/dist/assets/64PMEAB3-BjpCDGU9.js +8 -0
- package/web/dist/assets/6BEWHPQE-BpNa96Ze.js +2 -0
- package/web/dist/assets/6PIGVMFT-YXn9Hgnb.js +1 -0
- package/web/dist/assets/75JW6PN6-BFpDBuGN.js +2 -0
- package/web/dist/assets/7WLXJQOR-D6QVYUu9.js +1 -0
- package/web/dist/assets/7XWI72E6-BMuIspEy.js +2 -0
- package/web/dist/assets/A4UD2L5Z-BQzcD9dU.js +8 -0
- package/web/dist/assets/ArtifactPage-4GoBqSpU.js +1 -0
- package/web/dist/assets/ArtifactPane-Dx_0iWwH.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-CuHgARMk.js +6 -0
- package/web/dist/assets/BGW36YJW-CJMhya8S.js +4 -0
- package/web/dist/assets/CGIZI67J-DYDSLME8.js +2 -0
- package/web/dist/assets/DKTLEL5E-DciMwFJ1.js +1 -0
- package/web/dist/assets/DNNCRC2E-BIeKf5t-.js +5 -0
- package/web/dist/assets/Diagram-CZM_R7MG.js +1 -0
- package/web/dist/assets/Diagram-vdpmp3xu.css +1 -0
- package/web/dist/assets/E67EBMDD--AucIw59.js +5 -0
- package/web/dist/assets/EFYWJBJK-CVywe_D5.js +1 -0
- package/web/dist/assets/EXE5EH6O-DR1IHCTr.js +4 -0
- package/web/dist/assets/FTXIJQGB-BZVuiBn1.js +1 -0
- package/web/dist/assets/G3MDBVQH-BZmnMkFw.js +1 -0
- package/web/dist/assets/G6GPDDNC-DEz8F449.js +2 -0
- package/web/dist/assets/G76NGGFM-CrB2V34K.js +1 -0
- package/web/dist/assets/GH6G3TJC-D3RTYEH4.js +2 -0
- package/web/dist/assets/HPNUA7RO-B19NeNOJ.js +1 -0
- package/web/dist/assets/HQRGC5D7-Dmpzq6Rn.js +4 -0
- package/web/dist/assets/IX6Z2TVR-DphRHnek.js +5 -0
- package/web/dist/assets/J77NQRCJ-B7AGcxvw.js +3 -0
- package/web/dist/assets/JAD36DTZ-B9-dbsS0.js +5 -0
- package/web/dist/assets/LF3NJQAX-koL91UZ9.js +2 -0
- package/web/dist/assets/LRZCOFJZ-Cdjasfuf.js +6 -0
- package/web/dist/assets/Library-TwvrwqSk.js +2 -0
- package/web/dist/assets/Lightbox-C8KNKwOA.css +1 -0
- package/web/dist/assets/Lightbox-Du56MXGX.js +1 -0
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +4 -0
- package/web/dist/assets/N5NF2FGY-OuapYTfr.js +2 -0
- package/web/dist/assets/N7OWWQHY-BNLd-Q_U.js +2 -0
- package/web/dist/assets/NT5JY5WP-1lJM-aun.js +1 -0
- package/web/dist/assets/OT7W3RNY-jDL3drzX.js +4 -0
- package/web/dist/assets/P67P66DB-CE-opRb8.js +3 -0
- package/web/dist/assets/PFLLLM7K-DDJFgzW3.js +2 -0
- package/web/dist/assets/PUAMC4MJ-CtBm-WuU.js +12 -0
- package/web/dist/assets/PVFCHRLY-CG5JXv60.js +2 -0
- package/web/dist/assets/ProfilesDestination-rKNDpLjR.js +1 -0
- package/web/dist/assets/QQW2P742-BbcEkpa4.js +1 -0
- package/web/dist/assets/RVARI57O-CgA1WoRv.js +1 -0
- package/web/dist/assets/S63OEDPG-CKlhpvl2.js +3 -0
- package/web/dist/assets/SG5TVYLW-BBtiwXPL.js +2 -0
- package/web/dist/assets/TeamsPage-DtJylLrt.js +1 -0
- package/web/dist/assets/UGRSUGA2-urZ_hx83.js +5 -0
- package/web/dist/assets/V44WA6U7-DYfX3OlM.js +1 -0
- package/web/dist/assets/V67O4WWV-xYxJ00QW.js +1 -0
- package/web/dist/assets/VBUCM3YD-JHGjuJC_.js +1 -0
- package/web/dist/assets/VINGRCPA-DZuJa3SH.js +7 -0
- package/web/dist/assets/WL6DNKX4-BHqripdA.js +2 -0
- package/web/dist/assets/WQ63CJYP-D6G5frDY.js +2 -0
- package/web/dist/assets/Watchtower-Bmgdyfjb.js +2 -0
- package/web/dist/assets/XOYAOB3S-L-KeA1EF.js +13 -0
- package/web/dist/assets/Y5LQLG3M-dr2-erb8.js +2 -0
- package/web/dist/assets/YIK6JTN6--1t-Ud8k.js +2 -0
- package/web/dist/assets/artifacts-DU9uRgs1.js +1 -0
- package/web/dist/assets/index-C3ci5UtY.css +1 -0
- package/web/dist/assets/index-D-xG9nrc.js +36 -0
- package/web/dist/assets/lazy-f3CuUNJZ.js +43 -0
- package/web/dist/assets/live-7q72Lyj2.js +5 -0
- package/web/dist/assets/mermaid.worker-WDVRAh32.js +1 -0
- package/web/dist/assets/pane-layout-0RrLHGDr.js +1 -0
- package/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
- package/web/dist/assets/{renderer2d-CYag1CnR.js → renderer2d-hfZxM6Xl.js} +1 -1
- package/web/dist/assets/{renderer3d-BRB6rgDJ.js → renderer3d-Km9vipTM.js} +1 -1
- package/web/dist/assets/{safe-area-fOy0AOuG.js → safe-area-BAbgBiFk.js} +1 -1
- package/web/dist/assets/{silero-worker-DhlIHyD_.js → silero-worker-e802lB5T.js} +1 -1
- package/web/dist/assets/smart-turn-v3.2-cpu-WpeLRbPA.onnx +0 -0
- package/web/dist/assets/smart-turn-worker-DaVJPdvJ.js +11 -0
- package/web/dist/assets/source-BWlvaODa.js +1 -0
- package/web/dist/assets/toast-Cuu2K0hx.js +1 -0
- package/web/dist/assets/ui-CFB4wQfF.js +3 -0
- package/web/dist/index.html +8 -6
- package/web/src/voice/turn/LICENSE +28 -0
- package/web/dist/assets/ArtifactPage-Dq7VW-bU.js +0 -1
- package/web/dist/assets/ArtifactPane-DKwbMTX9.js +0 -1
- package/web/dist/assets/ArtifactWorkbench-vrgIfncX.js +0 -6
- package/web/dist/assets/Library-D6-Go6Cu.js +0 -2
- package/web/dist/assets/ProfilesDestination-BOML2Dgf.js +0 -1
- package/web/dist/assets/TeamsPage-CNBTE8gB.js +0 -1
- package/web/dist/assets/Watchtower-Dor1Ubjy.js +0 -2
- package/web/dist/assets/hub-CrbItiwP.js +0 -13
- package/web/dist/assets/index-82Ce0IAf.css +0 -1
- package/web/dist/assets/index-D9SP3_0z.js +0 -22
- package/web/dist/assets/live-BNJgy0vg.js +0 -5
- package/web/dist/assets/pane-layout-EdR1kRrG.js +0 -1
- package/web/dist/assets/preload-helper-DqrJTq0t.js +0 -1
- package/web/dist/assets/source-Wbq35qGp.js +0 -1
- package/web/dist/assets/ui-j5FFk90B.js +0 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// GET /api/media/unfurl?url=… (design §4, D6): a link's preview for the console, fetched by the hub through the safe
|
|
2
|
+
// fetcher (./fetcher.ts) for signed-in users only. The fetcher asks for HTML, reads at most the first
|
|
3
|
+
// UNFURL_MAX_BYTES (a longer page is cut, not refused) and only a 2xx text/html or application/xhtml+xml answer counts
|
|
4
|
+
// (else 415 not_html). ./unfurl-parse.ts turns it into plain text; the answer is JSON {url (the final URL, after
|
|
5
|
+
// redirects), title?, description?, siteName?, image?}, private and browser-cacheable for at most an hour. The image
|
|
6
|
+
// URL is never fetched here: the console loads it through /api/media/remote, and only for an allowlisted host.
|
|
7
|
+
// A tenant-scoped in-memory cache (per pod in cluster mode) keyed by the user and URL keeps previews for the site's
|
|
8
|
+
// lifetime clamped to 1-24 h and failures for 5 min. Per user: ONE fetch budget with /api/media/remote (30 a minute,
|
|
9
|
+
// 4 at once together, ./fetch-budget.ts); cache hits are free. Each fetch is audited with the user, the host and the
|
|
10
|
+
// outcome, never the full URL. HUB_REMOTE_FETCH=off turns it off (403 disabled).
|
|
11
|
+
import { createFetchBudget, type FetchBudget } from "./fetch-budget"
|
|
12
|
+
import { checkURL, FetchError, type Fetcher } from "./fetcher"
|
|
13
|
+
import { budgetMessage, failure, lifetime, type RemoteMediaAudit } from "./media-remote"
|
|
14
|
+
import { decodeHtml, parseUnfurl, type UnfurlMeta } from "./unfurl-parse"
|
|
15
|
+
|
|
16
|
+
/** Bytes of a page read at most (the rest is never downloaded). */
|
|
17
|
+
export const UNFURL_MAX_BYTES = 512 * 1024
|
|
18
|
+
/** The longest the browser may keep a preview (the hub's cache may keep it longer). */
|
|
19
|
+
const BROWSER_MAX_AGE_S = 3600
|
|
20
|
+
const FAILURE_TTL_MS = 5 * 60_000
|
|
21
|
+
const MAX_CACHE_ENTRIES = 2048
|
|
22
|
+
const ACCEPT = "text/html,application/xhtml+xml;q=0.9,*/*;q=0.1"
|
|
23
|
+
const HTML_TYPES = new Set(["text/html", "application/xhtml+xml"])
|
|
24
|
+
|
|
25
|
+
export interface Unfurled extends UnfurlMeta {
|
|
26
|
+
readonly url: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UnfurlOptions {
|
|
30
|
+
readonly fetcher: Fetcher
|
|
31
|
+
/** False: HUB_REMOTE_FETCH=off; every request is refused and nothing is fetched. */
|
|
32
|
+
readonly enabled: boolean
|
|
33
|
+
readonly now?: () => number
|
|
34
|
+
/** The hub's audit log (store.audit). */
|
|
35
|
+
readonly audit: (entry: RemoteMediaAudit) => unknown
|
|
36
|
+
/** The per-user fetch budget, shared with /api/media/remote (default: its own). */
|
|
37
|
+
readonly budget?: FetchBudget
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Unfurl {
|
|
41
|
+
/** Answers GET /api/media/unfurl for `userID` (already authenticated as a signed-in user). */
|
|
42
|
+
handle(userID: string, request: Request, url: URL): Promise<Response>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Entry =
|
|
46
|
+
| { readonly expires: number; readonly ok: true; readonly body: string }
|
|
47
|
+
| { readonly expires: number; readonly ok: false; readonly status: number; readonly code: string; readonly message: string }
|
|
48
|
+
|
|
49
|
+
export function createUnfurl(options: UnfurlOptions): Unfurl {
|
|
50
|
+
const now = options.now ?? Date.now
|
|
51
|
+
const budget = options.budget ?? createFetchBudget({ now })
|
|
52
|
+
const cache = new Map<string, Entry>()
|
|
53
|
+
|
|
54
|
+
function remember(key: string, entry: Entry): void {
|
|
55
|
+
cache.delete(key)
|
|
56
|
+
cache.set(key, entry)
|
|
57
|
+
while (cache.size > MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value!)
|
|
58
|
+
}
|
|
59
|
+
function lookup(key: string): Entry | undefined {
|
|
60
|
+
const entry = cache.get(key)
|
|
61
|
+
if (!entry) return undefined
|
|
62
|
+
cache.delete(key)
|
|
63
|
+
if (entry.expires <= now()) return undefined
|
|
64
|
+
cache.set(key, entry) // most recently used last
|
|
65
|
+
return entry
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function answer(entry: Entry): Response {
|
|
69
|
+
if (!entry.ok) return failure(entry.status, entry.code, entry.message)
|
|
70
|
+
const maxAge = Math.max(0, Math.min(BROWSER_MAX_AGE_S, Math.floor((entry.expires - now()) / 1000)))
|
|
71
|
+
return new Response(entry.body, { headers: { "content-type": "application/json; charset=utf-8", "cache-control": `private, max-age=${maxAge}`, "x-content-type-options": "nosniff" } })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function load(target: URL): Promise<{ entry: Entry; host: string; outcome: string }> {
|
|
75
|
+
const at = now()
|
|
76
|
+
const fail = (status: number, code: string, message: string): Entry => ({ expires: at + FAILURE_TTL_MS, ok: false, status, code, message })
|
|
77
|
+
try {
|
|
78
|
+
const got = await options.fetcher.fetch(target, { maxBytes: UNFURL_MAX_BYTES, truncate: true, accept: ACCEPT })
|
|
79
|
+
const host = got.url.hostname
|
|
80
|
+
const contentType = got.headers.get("content-type")
|
|
81
|
+
const mime = (contentType ?? "").split(";")[0]!.trim().toLowerCase()
|
|
82
|
+
if (!HTML_TYPES.has(mime)) return { entry: fail(415, "not_html", "The link is not a web page (HTML)"), host, outcome: "not_html" }
|
|
83
|
+
const meta = parseUnfurl(decodeHtml(got.body, contentType), got.url)
|
|
84
|
+
const body: Unfurled = { url: got.url.href, ...meta }
|
|
85
|
+
return { entry: { expires: at + lifetime(got.headers, at), ok: true, body: JSON.stringify(body) }, host, outcome: "ok" }
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
if (!(cause instanceof FetchError)) throw cause
|
|
88
|
+
return { entry: fail(cause.status, cause.kind, cause.message), host: cause.host ?? target.hostname, outcome: cause.kind }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handle(userID: string, _request: Request, url: URL): Promise<Response> {
|
|
93
|
+
if (!options.enabled) return failure(403, "disabled", "Link previews are turned off on this hub (HUB_REMOTE_FETCH=off)")
|
|
94
|
+
const raw = url.searchParams.get("url")
|
|
95
|
+
if (!raw) return failure(400, "bad_url", "Expected a url query parameter")
|
|
96
|
+
let target: URL
|
|
97
|
+
try {
|
|
98
|
+
target = checkURL(raw)
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
if (cause instanceof FetchError) return failure(cause.status, cause.kind, cause.message)
|
|
101
|
+
throw cause
|
|
102
|
+
}
|
|
103
|
+
const key = `${userID}\n${target.href}`
|
|
104
|
+
const hit = lookup(key)
|
|
105
|
+
if (hit) return answer(hit)
|
|
106
|
+
const slot = budget.take(userID)
|
|
107
|
+
if (!slot.ok) return failure(429, "rate_limited", budgetMessage(slot), { "retry-after": String(slot.retryAfter) })
|
|
108
|
+
try {
|
|
109
|
+
const { entry, host, outcome } = await load(target)
|
|
110
|
+
await Promise.resolve(options.audit({ actor: userID, action: "media.unfurl", target: host, detail: outcome })).catch(() => undefined)
|
|
111
|
+
remember(key, entry)
|
|
112
|
+
return answer(entry)
|
|
113
|
+
} finally {
|
|
114
|
+
slot.release()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { handle }
|
|
119
|
+
}
|
|
@@ -80,41 +80,9 @@ export function renderTemplate(template: string, values: Readonly<Record<string,
|
|
|
80
80
|
return out
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
["hub app token", /\bhat_[0-9a-f]{16}_[A-Za-z0-9_-]{43,}/],
|
|
87
|
-
["github token", /\bgh[pousr]_[A-Za-z0-9]{36,}/],
|
|
88
|
-
["github token", /\bgithub_pat_[A-Za-z0-9_]{22,}/],
|
|
89
|
-
["api key", /\bsk-[A-Za-z0-9_-]{20,}/],
|
|
90
|
-
["api key", /\bAIza[0-9A-Za-z_-]{35}/],
|
|
91
|
-
["aws access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/],
|
|
92
|
-
["slack token", /\bxox[abposr]-[A-Za-z0-9-]{10,}/],
|
|
93
|
-
["jwt", /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/],
|
|
94
|
-
["bearer token", /\b[Bb]earer\s+[A-Za-z0-9._~+/=-]{20,}/],
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
/** The kind of literal credential in a text, or undefined. */
|
|
98
|
-
export function credentialFinding(text: string): string | undefined {
|
|
99
|
-
for (const [kind, pattern] of CREDENTIALS) if (pattern.test(text)) return kind
|
|
100
|
-
return undefined
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* (REC-01) Replaces every literal credential in a text with `[redacted: <kind>]` (the CREDENTIALS table, applied
|
|
105
|
-
* globally) and lists the kinds removed, each once, in table order. Voice recordings scrub every text field with it.
|
|
106
|
-
*/
|
|
107
|
-
export function redactCredentials(text: string): { text: string; kinds: string[] } {
|
|
108
|
-
const kinds: string[] = []
|
|
109
|
-
let out = text
|
|
110
|
-
for (const [kind, pattern] of CREDENTIALS) {
|
|
111
|
-
const global = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`)
|
|
112
|
-
const next = out.replace(global, `[redacted: ${kind}]`)
|
|
113
|
-
if (next !== out && !kinds.includes(kind)) kinds.push(kind)
|
|
114
|
-
out = next
|
|
115
|
-
}
|
|
116
|
-
return { text: out, kinds }
|
|
117
|
-
}
|
|
83
|
+
// (REC-01) The credential table and scrubber live in the protocol package (the spoke scrubs voice-front wakes too).
|
|
84
|
+
import { credentialFinding, redactCredentials } from "../../../packages/protocol/src/credentials.ts"
|
|
85
|
+
export { credentialFinding, redactCredentials }
|
|
118
86
|
|
|
119
87
|
/** Fields that would run code or install things on a machine: refused as unsupported. */
|
|
120
88
|
const HOOK_FIELDS = new Set(["hooks", "hook", "setup", "install", "postinstall", "preinstall", "dependencies", "devdependencies", "requirements", "scripts"])
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// is a console route with access "user": the user's cookie or device token only (the hub refuses session, app and
|
|
3
3
|
// act tokens before these run), so no agent, not even the Concierge, can read or export a recording, and there is
|
|
4
4
|
// no admin route. Every lookup is by (user.id, id); another user's, an unknown and a deleted ID answer 404 alike.
|
|
5
|
-
import { RECORDING_AUDIO_TYPE, RECORDING_LIMITS, type RecordingClient } from "../../protocol"
|
|
5
|
+
import { RECORDING_AUDIO_TYPE, RECORDING_FEATURES, RECORDING_LIMITS, type RecordingClient } from "../../protocol"
|
|
6
6
|
import { readBounded } from "../agent/body"
|
|
7
7
|
import { checkConsoleBatch } from "./schema"
|
|
8
8
|
import { audioKey, blobTenant, isSegmentNumber, summaryOf, type AudioEntry } from "./store"
|
|
@@ -199,7 +199,7 @@ export function recordingRoutes(service: RecordingService, options: { readonly v
|
|
|
199
199
|
return fail(429, `At most ${L.recordingsStored} recordings are kept; delete some in Settings → Voice`, "limit", { limit: L.recordingsStored })
|
|
200
200
|
}
|
|
201
201
|
const record = started.record
|
|
202
|
-
return Response.json({ recording: summaryOf(record), audio: record.audio, hubAt: options.now() }, { status: started.kind === "created" ? 201 : 200, headers: NO_STORE })
|
|
202
|
+
return Response.json({ recording: summaryOf(record), audio: record.audio, hubAt: options.now(), features: [...RECORDING_FEATURES] }, { status: started.kind === "created" ? 201 : 200, headers: NO_STORE })
|
|
203
203
|
}),
|
|
204
204
|
route("GET", "/api/recordings", async ({ url, user }) => {
|
|
205
205
|
const limitRaw = url.searchParams.get("limit")
|
|
@@ -45,7 +45,7 @@ export const CONSOLE_FIELDS: Readonly<Record<ConsoleEventKind, Fields>> = {
|
|
|
45
45
|
"stt.received": { segment: int, "text?": text(L.textChars), "junk?": yes, "empty?": yes, "failed?": text(L.messageChars), "audio?": oneOf(...AUDIO_RESULTS) },
|
|
46
46
|
"utterance.held": { text: text(L.utteranceChars) },
|
|
47
47
|
"utterance.merged": { segments: list(int, 1000), pauseMs: num },
|
|
48
|
-
"utterance.sent": { text: text(L.utteranceChars), segments: list(int, 1000), reason: oneOf("finished", "hold", "max", "command", "typed"), holdMs: num },
|
|
48
|
+
"utterance.sent": { text: text(L.utteranceChars), segments: list(int, 1000), reason: oneOf("finished", "hold", "max", "command", "typed", "smart", "smart-fallback"), holdMs: num },
|
|
49
49
|
"utterance.accepted": { inboxID: id },
|
|
50
50
|
"utterance.failed": { "status?": int, message: text(L.messageChars) },
|
|
51
51
|
"utterance.command": { command: oneOf("pause", "interrupt"), text: text(L.textChars) },
|
|
@@ -68,6 +68,8 @@ export const CONSOLE_FIELDS: Readonly<Record<ConsoleEventKind, Fields>> = {
|
|
|
68
68
|
mic: { live: bool, "reason?": label },
|
|
69
69
|
control: { action: oneOf("pause", "resume", "hush", "background", "cues"), "on?": bool },
|
|
70
70
|
"turn.timing": { "speechEnd?": num, "transcript?": num, "accepted?": num, "firstSay?": num, "firstByte?": num, firstAudio: num },
|
|
71
|
+
// Feature "turn.predict" (Smart Turn): a score (none when the model failed or was slow) and what it decided.
|
|
72
|
+
"turn.predict": { "p?": num, ms: num, silenceMs: num, decision: oneOf("send", "hold", "fallback", "timer-fallback"), "error?": text(L.messageChars) },
|
|
71
73
|
"decision.voice": { decisionID: id, option: id, outcome: oneOf("answered", "refused", "failed") },
|
|
72
74
|
"clock.sync": { t0: num, t1: num, hubAt: num },
|
|
73
75
|
error: { where: oneOf("stt", "tts", "send", "mic", "upload", "audio"), message: text(L.messageChars) },
|