@huynle/hub 0.7.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +430 -1
- package/Dockerfile +7 -2
- package/README.md +230 -32
- package/package.json +5 -3
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/daemon.ts +8 -1
- package/packages/claude-code/src/launcher.ts +29 -9
- package/packages/claude-code/src/paths.ts +3 -3
- package/packages/claude-code/src/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6871 -3460
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
- package/packages/cli/src/artifacts-mcp.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
- package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
- package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
- package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +96 -14
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +2 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/launches.ts +9 -3
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +385 -10
- package/packages/cli/src/messaging.ts +7 -3
- package/packages/cli/src/profiles-mcp.ts +301 -0
- package/packages/cli/src/setup/commands.ts +23 -4
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/spoke-services.ts +1 -1
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +460 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/paths.ts +3 -3
- package/packages/codex/src/profile.ts +163 -0
- package/packages/codex/src/sessions.ts +12 -0
- package/packages/codex/src/spoke.ts +19 -1
- package/packages/node/package.json +1 -1
- package/packages/node/src/config.ts +104 -2
- package/packages/node/src/opencode-warm.ts +344 -0
- package/packages/node/src/run.ts +28 -1
- package/packages/preset/package.json +1 -1
- package/packages/preset/src/profile.ts +34 -0
- package/packages/preset/src/spoke.ts +4 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/artifacts.ts +142 -0
- package/packages/protocol/src/concierge.ts +366 -0
- package/packages/protocol/src/contracts.ts +104 -0
- package/packages/protocol/src/graph.ts +70 -0
- package/packages/protocol/src/hub-workspace.ts +201 -0
- package/packages/protocol/src/index.ts +175 -6
- package/packages/protocol/src/launch.ts +36 -0
- package/packages/protocol/src/profiles.ts +301 -0
- package/packages/protocol/src/teams.ts +204 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/address.ts +50 -2
- package/packages/sdk/src/artifact-jobs.ts +194 -0
- package/packages/sdk/src/artifacts.ts +187 -0
- package/packages/sdk/src/client.ts +226 -5
- package/packages/sdk/src/concierge.ts +88 -0
- package/packages/sdk/src/errors.ts +4 -1
- package/packages/sdk/src/index.ts +6 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +568 -1
- package/packages/sdk/src/wake.ts +20 -0
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/index.ts +1 -0
- package/packages/spoke-sdk/src/profile-apply.ts +245 -0
- package/src/central/actions.ts +7 -0
- package/src/central/agent/acting-routes.ts +2 -1
- package/src/central/agent/acting-switch.ts +2 -1
- package/src/central/agent/acting.ts +218 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +18 -1
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +125 -2
- package/src/central/agent/delivery.ts +194 -40
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +7 -0
- package/src/central/agent/escalation.ts +10 -2
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +18 -4
- package/src/central/agent/form-check.ts +85 -0
- package/src/central/agent/grant-requests.ts +2 -1
- package/src/central/agent/grant-routes.ts +4 -3
- package/src/central/agent/grants.ts +29 -2
- package/src/central/agent/idempotency.ts +133 -0
- package/src/central/agent/intent-routes.ts +116 -0
- package/src/central/agent/intents.ts +505 -0
- package/src/central/agent/interaction-graph-routes.ts +212 -0
- package/src/central/agent/interaction-graph.ts +865 -0
- package/src/central/agent/launch-engine.ts +189 -32
- package/src/central/agent/launch-routes.ts +26 -10
- package/src/central/agent/launch-spec.ts +40 -3
- package/src/central/agent/launch-store.ts +55 -2
- package/src/central/agent/message-limits.ts +89 -11
- package/src/central/agent/message-routes.ts +10 -4
- package/src/central/agent/messages.ts +28 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/placement.ts +93 -18
- package/src/central/agent/read-routes.ts +89 -19
- package/src/central/agent/routes.ts +178 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- package/src/central/agent/webhooks.ts +17 -2
- package/src/central/agent-data.ts +4 -0
- package/src/central/artifacts/access.ts +44 -0
- package/src/central/artifacts/blob-store.ts +113 -0
- package/src/central/artifacts/blob-support.ts +98 -0
- package/src/central/artifacts/events.ts +27 -0
- package/src/central/artifacts/host-caps.ts +162 -0
- package/src/central/artifacts/host.ts +315 -0
- package/src/central/artifacts/idempotency.ts +109 -0
- package/src/central/artifacts/jobs-routes.ts +776 -0
- package/src/central/artifacts/jobs.ts +344 -0
- package/src/central/artifacts/maintenance.ts +103 -0
- package/src/central/artifacts/routes.ts +360 -0
- package/src/central/artifacts/store.ts +776 -0
- package/src/central/artifacts/workspaces.ts +31 -0
- package/src/central/backend/file-blobs.ts +377 -0
- package/src/central/backend/index.ts +12 -3
- package/src/central/backend/memory-blobs.ts +116 -0
- package/src/central/backend/postgres-blobs.ts +513 -0
- package/src/central/backend/postgres-migrate.ts +33 -0
- package/src/central/backend/postgres-state.ts +19 -0
- package/src/central/backend/types.ts +8 -0
- package/src/central/concierge/activity.ts +142 -0
- package/src/central/concierge/arm.ts +63 -0
- package/src/central/concierge/board-render.ts +190 -0
- package/src/central/concierge/board-results.ts +169 -0
- package/src/central/concierge/board-routes.ts +350 -0
- package/src/central/concierge/board.ts +249 -0
- package/src/central/concierge/context.ts +221 -0
- package/src/central/concierge/entitlements.ts +13 -0
- package/src/central/concierge/guard.ts +120 -0
- package/src/central/concierge/inbound.ts +86 -0
- package/src/central/concierge/index.ts +84 -0
- package/src/central/concierge/notes.ts +41 -0
- package/src/central/concierge/notifier.ts +986 -0
- package/src/central/concierge/prompt.ts +131 -0
- package/src/central/concierge/proposals.ts +268 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +122 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +125 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +767 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +279 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +28 -6
- package/src/central/http.ts +660 -54
- package/src/central/main.ts +1 -1
- package/src/central/mcp-http.ts +10 -21
- package/src/central/profiles/apply.ts +333 -0
- package/src/central/profiles/content.ts +174 -0
- package/src/central/profiles/events.ts +40 -0
- package/src/central/profiles/idempotency.ts +14 -0
- package/src/central/profiles/import.ts +139 -0
- package/src/central/profiles/routes.ts +356 -0
- package/src/central/profiles/store.ts +597 -0
- package/src/central/profiles/usage-ports.ts +120 -0
- package/src/central/profiles/usage.ts +364 -0
- package/src/central/push.ts +7 -0
- package/src/central/registry.ts +69 -7
- package/src/central/store.ts +4 -0
- package/src/central/teams/collections.ts +34 -0
- package/src/central/teams/discovery.ts +346 -0
- package/src/central/teams/draft-edits.ts +46 -0
- package/src/central/teams/events.ts +46 -0
- package/src/central/teams/intake-routes.ts +100 -0
- package/src/central/teams/intake.ts +786 -0
- package/src/central/teams/policy.ts +231 -0
- package/src/central/teams/routes.ts +516 -0
- package/src/central/teams/seats.ts +272 -0
- package/src/central/teams/staffing.ts +910 -0
- package/src/central/teams/store.ts +667 -0
- package/src/central/teams/validation.ts +256 -0
- package/src/central/workspaces/board.ts +408 -0
- package/src/central/workspaces/checkpoints.ts +316 -0
- package/src/central/workspaces/coordinator.ts +841 -0
- package/src/central/workspaces/events.ts +85 -0
- package/src/central/workspaces/lifecycle.ts +93 -0
- package/src/central/workspaces/master.ts +490 -0
- package/src/central/workspaces/members.ts +632 -0
- package/src/central/workspaces/routes.ts +354 -0
- package/src/central/workspaces/sidechats-routes.ts +410 -0
- package/src/central/workspaces/sidechats.ts +482 -0
- package/src/central/workspaces/store.ts +571 -0
- package/src/central/workspaces/supervision.ts +355 -0
- package/src/index.ts +126 -23
- package/src/migrate.ts +109 -3
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/env-inject.ts +8 -1
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +404 -25
- package/src/spoke/launchers.ts +6 -4
- package/src/spoke/listing.ts +65 -10
- package/src/spoke/localhub.ts +14 -4
- package/src/spoke/locations.ts +40 -0
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +256 -0
- package/src/spoke/spoke.ts +330 -23
- package/src/spoke/timeline.ts +6 -1
- package/src/spoke/tracker.ts +23 -1
- package/src/spoke/who-teams.ts +81 -0
- package/src/tui-messaging.ts +32 -18
- package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
- package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
- package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
- package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
- package/web/dist/assets/index-BabrNDB3.css +1 -0
- package/web/dist/assets/index-lIOfSYck.js +34 -0
- package/web/dist/assets/live-BDq0AABg.js +5 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
- package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
- package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
- package/web/dist/assets/source-CDU04kYw.js +1 -0
- package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
- package/web/dist/index.html +5 -2
- package/web/dist/sw.js +22 -2
- package/web/dist/assets/index-Dm8OmkaO.css +0 -1
- package/web/dist/assets/index-FUJbj_Du.js +0 -20
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// CON-04 Concierge settings (contract "Concierge service: settings, launch, supervise, hide, context"): one record
|
|
2
|
+
// per user (collection `concierge`, key `settings`), edited only by the user (Settings → Concierge). A partial update
|
|
3
|
+
// names `expectedRevision` (409 conflict {currentRevision} on a mismatch); unknown and read-only fields (enabled,
|
|
4
|
+
// grantID, revision, updatedAt) are 400 invalid: enabling goes through POST /concierge/enable, the grant ID is the
|
|
5
|
+
// hub's. `where: "hosted"` is reserved for C2 (422 unsupported {feature: "conciergeHosted"}). The grant spec becomes
|
|
6
|
+
// a real grant held by @@concierge (grantInput); `launch` and `roles` are refused in C1. No grant is the default (a
|
|
7
|
+
// read-and-notify Concierge); PRESET_GRANT is the conservative one Settings offers.
|
|
8
|
+
import { CONCIERGE_LIMITS, CONCIERGE_ROLE, type ConciergeDigest, type ConciergeGrantSpec, type ConciergeGrantVerb, type ConciergeQuietHours, type ConciergeSettings } from "../../../packages/protocol/src/concierge"
|
|
9
|
+
import type { Clock, StateStore } from "../backend/types"
|
|
10
|
+
import { AgentError } from "../agent/errors"
|
|
11
|
+
import { canonicalProject, GRANT_MAX_ACTIONS_PER_HOUR, GRANT_PROJECTS_MAX, parseDuration, validateGrant } from "../agent/policy"
|
|
12
|
+
import { CONCIERGE_COLLECTION } from "./notes"
|
|
13
|
+
|
|
14
|
+
const KEY = "settings"
|
|
15
|
+
export const BRAIN_UNSUPPORTED = "Brain is not available to the Concierge yet: its tools are an exact allow-list, and Brain's MCP tools cannot be pinned by name"
|
|
16
|
+
const FIELDS = new Set(["where", "machineID", "directory", "model", "grant", "connectors", "timeZone", "quietHours", "quietAllowsUrgent", "digests", "expectedRevision"])
|
|
17
|
+
const GRANT_FIELDS = new Set(["allow", "projects", "permissions", "allowAlways", "expires", "maxActionsPerHour"])
|
|
18
|
+
export const CONCIERGE_GRANT_VERBS: readonly ConciergeGrantVerb[] = ["approve", "deny", "answer", "prompt", "stop"]
|
|
19
|
+
const MACHINE = /^[A-Za-z0-9._-]{1,80}$/
|
|
20
|
+
const MODEL = /^[A-Za-z0-9._-]{1,80}\/[!-~]{1,200}$/
|
|
21
|
+
const CLOCK = /^([01]\d|2[0-3]):[0-5]\d$/
|
|
22
|
+
const EXPIRES = /^(\d{1,4})([hd])$/
|
|
23
|
+
const DIRECTORY_MAX = 4096
|
|
24
|
+
const HOUR_MS = 3_600_000
|
|
25
|
+
const EXPIRES_MAX_MS = 30 * 24 * HOUR_MS
|
|
26
|
+
|
|
27
|
+
const invalid = (message: string) => new AgentError("invalid", message)
|
|
28
|
+
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
29
|
+
|
|
30
|
+
export function defaultSettings(timeZone = "UTC"): ConciergeSettings {
|
|
31
|
+
return { enabled: false, where: "machine", connectors: { brain: false }, timeZone, quietAllowsUrgent: true, digests: [], revision: 0 }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The conservative grant Settings offers: answer and deny; approve read/edit inside the projects; never shell push/rm; 7 days. */
|
|
35
|
+
export const PRESET_GRANT = (projects: readonly string[]): ConciergeGrantSpec => ({
|
|
36
|
+
allow: ["answer", "deny", "approve"],
|
|
37
|
+
projects: [...projects],
|
|
38
|
+
permissions: { allow: ["read", "edit"], never: ["shell:git push*", "shell:rm *"] },
|
|
39
|
+
allowAlways: false,
|
|
40
|
+
expires: "7d",
|
|
41
|
+
maxActionsPerHour: 60,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/** The grant routes' body for a Concierge grant spec (holder @@concierge). */
|
|
45
|
+
export function grantInput(spec: ConciergeGrantSpec): Record<string, unknown> {
|
|
46
|
+
return {
|
|
47
|
+
holder: `@@${CONCIERGE_ROLE}`,
|
|
48
|
+
scope: { projects: [...spec.projects] },
|
|
49
|
+
allow: [...spec.allow],
|
|
50
|
+
permissions: { allow: [...spec.permissions.allow], never: [...spec.permissions.never] },
|
|
51
|
+
// (CON-13 decision) Never "always" for the Concierge, whatever an older stored spec says.
|
|
52
|
+
allowAlways: false,
|
|
53
|
+
expires: spec.expires,
|
|
54
|
+
maxActionsPerHour: spec.maxActionsPerHour,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validTimeZone(value: unknown): value is string {
|
|
59
|
+
if (typeof value !== "string" || !value || value.length > 64) return false
|
|
60
|
+
try {
|
|
61
|
+
new Intl.DateTimeFormat("en-GB", { timeZone: value })
|
|
62
|
+
return true
|
|
63
|
+
} catch {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function grantSpec(raw: unknown): ConciergeGrantSpec {
|
|
69
|
+
if (!isObject(raw)) throw invalid("grant is {allow, projects, permissions?, allowAlways?, expires?, maxActionsPerHour?}")
|
|
70
|
+
for (const key of Object.keys(raw)) if (!GRANT_FIELDS.has(key)) throw invalid(`Unknown grant field ${key.slice(0, 40)}`)
|
|
71
|
+
if (!Array.isArray(raw.allow) || raw.allow.length === 0) throw invalid(`grant.allow lists the verbs: ${CONCIERGE_GRANT_VERBS.join(", ")}`)
|
|
72
|
+
const allow: ConciergeGrantVerb[] = []
|
|
73
|
+
for (const verb of raw.allow) {
|
|
74
|
+
if (verb === "launch" || verb === "roles") throw invalid(`grant.allow: the Concierge cannot hold ${verb} in this version`)
|
|
75
|
+
if (!CONCIERGE_GRANT_VERBS.includes(verb)) throw invalid(`grant.allow: unknown verb ${String(verb).slice(0, 40)}`)
|
|
76
|
+
if (!allow.includes(verb)) allow.push(verb)
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(raw.projects) || raw.projects.length === 0 || raw.projects.length > GRANT_PROJECTS_MAX) throw invalid(`grant.projects lists 1–${GRANT_PROJECTS_MAX} project directories`)
|
|
79
|
+
const projects: string[] = []
|
|
80
|
+
for (const item of raw.projects) {
|
|
81
|
+
const canonical = canonicalProject(item as string)
|
|
82
|
+
if (canonical === undefined) throw invalid(`grant.projects: ${String(item).slice(0, 80)} is not an absolute project directory`)
|
|
83
|
+
if (!projects.includes(canonical)) projects.push(canonical)
|
|
84
|
+
}
|
|
85
|
+
let permissions = { allow: [] as string[], never: [] as string[] }
|
|
86
|
+
if (raw.permissions !== undefined) {
|
|
87
|
+
if (!isObject(raw.permissions)) throw invalid("grant.permissions is {allow?, never?}")
|
|
88
|
+
permissions = { allow: (raw.permissions.allow ?? []) as string[], never: (raw.permissions.never ?? []) as string[] }
|
|
89
|
+
}
|
|
90
|
+
if (raw.allowAlways !== undefined && typeof raw.allowAlways !== "boolean") throw invalid("grant.allowAlways is true or false")
|
|
91
|
+
if (raw.allowAlways === true) throw invalid("grant.allowAlways: the Concierge never approves “always” (that is always your tap)")
|
|
92
|
+
const expires = raw.expires ?? "30d"
|
|
93
|
+
const match = typeof expires === "string" ? EXPIRES.exec(expires) : null
|
|
94
|
+
const ms = match ? parseDuration(expires) : undefined
|
|
95
|
+
if (ms === undefined || ms < HOUR_MS || ms > EXPIRES_MAX_MS) throw invalid('grant.expires is "1h" to "30d"')
|
|
96
|
+
const cap = raw.maxActionsPerHour ?? 60
|
|
97
|
+
if (!Number.isInteger(cap) || (cap as number) < 1 || (cap as number) > GRANT_MAX_ACTIONS_PER_HOUR) throw invalid(`grant.maxActionsPerHour is a whole number 1–${GRANT_MAX_ACTIONS_PER_HOUR}`)
|
|
98
|
+
const spec: ConciergeGrantSpec = { allow, projects, permissions, allowAlways: false, expires: expires as string, maxActionsPerHour: cap as number }
|
|
99
|
+
// The grant validator has the last word on patterns (the same rules as every grant).
|
|
100
|
+
const checked = validateGrant(grantInput(spec))
|
|
101
|
+
return { ...spec, permissions: { allow: [...checked.permissions.allow], never: [...checked.permissions.never] } }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function quietHours(raw: unknown): ConciergeQuietHours {
|
|
105
|
+
if (!isObject(raw) || Object.keys(raw).some((k) => !["start", "end", "timeZone"].includes(k))) throw invalid("quietHours is {start, end, timeZone}")
|
|
106
|
+
if (typeof raw.start !== "string" || !CLOCK.test(raw.start) || typeof raw.end !== "string" || !CLOCK.test(raw.end)) throw invalid("quietHours start and end are HH:MM")
|
|
107
|
+
if (!validTimeZone(raw.timeZone)) throw invalid("quietHours.timeZone is an IANA time zone")
|
|
108
|
+
return { start: raw.start, end: raw.end, timeZone: raw.timeZone }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function digests(raw: unknown): ConciergeDigest[] {
|
|
112
|
+
if (!Array.isArray(raw) || raw.length > CONCIERGE_LIMITS.digests) throw invalid(`digests lists at most ${CONCIERGE_LIMITS.digests} entries`)
|
|
113
|
+
return raw.map((d) => {
|
|
114
|
+
if (!isObject(d) || Object.keys(d).some((k) => k !== "at" && k !== "days")) throw invalid("a digest is {at, days?}")
|
|
115
|
+
if (typeof d.at !== "string" || !CLOCK.test(d.at)) throw invalid("digest.at is HH:MM")
|
|
116
|
+
if (d.days === undefined) return { at: d.at }
|
|
117
|
+
if (!Array.isArray(d.days) || d.days.length === 0 || d.days.length > 7 || d.days.some((n) => !Number.isInteger(n) || n < 0 || n > 6) || new Set(d.days).size !== d.days.length) {
|
|
118
|
+
throw invalid("digest.days lists weekdays 0–6 (Sunday is 0)")
|
|
119
|
+
}
|
|
120
|
+
return { at: d.at, days: [...(d.days as (0 | 1 | 2 | 3 | 4 | 5 | 6)[])].sort() }
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The settings after a partial update by the user. Throws invalid, unsupported (hosted) or conflict (revision).
|
|
128
|
+
* Does not bump the revision (the store does).
|
|
129
|
+
*/
|
|
130
|
+
export function parseSettingsPatch(body: unknown, current: ConciergeSettings): ConciergeSettings {
|
|
131
|
+
if (!isObject(body)) throw invalid("The body is a partial settings object with expectedRevision")
|
|
132
|
+
for (const key of Object.keys(body)) {
|
|
133
|
+
if (key === "enabled") throw invalid("enabled changes through POST /api/agent/concierge/enable or /disable")
|
|
134
|
+
if (!FIELDS.has(key)) throw invalid(`Unknown or read-only field ${key.slice(0, 40)}`)
|
|
135
|
+
}
|
|
136
|
+
const expected = body.expectedRevision
|
|
137
|
+
if (!Number.isSafeInteger(expected)) throw invalid("expectedRevision is required")
|
|
138
|
+
if (expected !== current.revision) throw new AgentError("conflict", "The Concierge settings changed meanwhile", { extra: { currentRevision: current.revision } })
|
|
139
|
+
let next: { -readonly [K in keyof ConciergeSettings]: ConciergeSettings[K] } = { ...current }
|
|
140
|
+
const drop = (key: "machineID" | "directory" | "model" | "grant" | "quietHours") => {
|
|
141
|
+
const { [key]: _gone, ...rest } = next
|
|
142
|
+
next = rest as typeof next
|
|
143
|
+
}
|
|
144
|
+
if (body.where !== undefined) {
|
|
145
|
+
if (body.where === "hosted") throw new AgentError("unsupported", "A hosted Concierge is not available", { extra: { feature: "conciergeHosted" } })
|
|
146
|
+
if (body.where !== "machine") throw invalid('where is "machine"')
|
|
147
|
+
}
|
|
148
|
+
if (body.machineID !== undefined) {
|
|
149
|
+
if (body.machineID === null) drop("machineID")
|
|
150
|
+
else if (typeof body.machineID !== "string" || !MACHINE.test(body.machineID)) throw invalid("machineID is malformed")
|
|
151
|
+
else next.machineID = body.machineID
|
|
152
|
+
}
|
|
153
|
+
if (body.directory !== undefined) {
|
|
154
|
+
if (body.directory === null) drop("directory")
|
|
155
|
+
else {
|
|
156
|
+
const raw = body.directory
|
|
157
|
+
const canonical = typeof raw === "string" && raw.length <= DIRECTORY_MAX && !CONTROL.test(raw) ? canonicalProject(raw) : undefined
|
|
158
|
+
if (canonical === undefined || !canonical.startsWith("/")) throw invalid("directory is an absolute directory on the machine (a git repo under its workspace roots)")
|
|
159
|
+
next.directory = canonical
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (body.model !== undefined) {
|
|
163
|
+
if (body.model === null) drop("model")
|
|
164
|
+
else if (typeof body.model !== "string" || !MODEL.test(body.model)) throw invalid("model is provider/model")
|
|
165
|
+
else next.model = body.model
|
|
166
|
+
}
|
|
167
|
+
if (body.grant !== undefined) {
|
|
168
|
+
if (body.grant === null) drop("grant")
|
|
169
|
+
else next.grant = grantSpec(body.grant)
|
|
170
|
+
}
|
|
171
|
+
if (body.connectors !== undefined) {
|
|
172
|
+
const c = body.connectors
|
|
173
|
+
if (!isObject(c) || Object.keys(c).some((k) => k !== "brain") || typeof c.brain !== "boolean") throw invalid("connectors is {brain: true|false}")
|
|
174
|
+
// The Concierge's tools are an exact allow-list; Brain's MCP tools cannot be named exactly across machines (and
|
|
175
|
+
// Code Mode would expose every MCP tool), so Brain is not offered in C1.
|
|
176
|
+
if (c.brain) throw new AgentError("unsupported", BRAIN_UNSUPPORTED, { extra: { feature: "conciergeBrain", reason: BRAIN_UNSUPPORTED } })
|
|
177
|
+
next.connectors = { brain: false }
|
|
178
|
+
}
|
|
179
|
+
if (body.timeZone !== undefined) {
|
|
180
|
+
if (!validTimeZone(body.timeZone)) throw invalid("timeZone is an IANA time zone")
|
|
181
|
+
next.timeZone = body.timeZone
|
|
182
|
+
}
|
|
183
|
+
if (body.quietHours !== undefined) {
|
|
184
|
+
if (body.quietHours === null) drop("quietHours")
|
|
185
|
+
else next.quietHours = quietHours(body.quietHours)
|
|
186
|
+
}
|
|
187
|
+
if (body.quietAllowsUrgent !== undefined) {
|
|
188
|
+
if (typeof body.quietAllowsUrgent !== "boolean") throw invalid("quietAllowsUrgent is true or false")
|
|
189
|
+
next.quietAllowsUrgent = body.quietAllowsUrgent
|
|
190
|
+
}
|
|
191
|
+
if (body.digests !== undefined) next.digests = digests(body.digests)
|
|
192
|
+
return next
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The user's settings record: version-checked writes, revision bumped by every write. */
|
|
196
|
+
export class SettingsStore {
|
|
197
|
+
constructor(
|
|
198
|
+
private readonly state: StateStore,
|
|
199
|
+
private readonly now: Clock = Date.now,
|
|
200
|
+
) {}
|
|
201
|
+
|
|
202
|
+
async get(userID: string): Promise<ConciergeSettings> {
|
|
203
|
+
return (await this.state.get<ConciergeSettings>(userID, CONCIERGE_COLLECTION, KEY))?.value ?? defaultSettings()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Applies `change` to the stored settings (re-read on a lost race; `change` may throw) and stores revision + 1. */
|
|
207
|
+
async update(userID: string, change: (current: ConciergeSettings) => ConciergeSettings): Promise<ConciergeSettings> {
|
|
208
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
209
|
+
const stored = await this.state.get<ConciergeSettings>(userID, CONCIERGE_COLLECTION, KEY)
|
|
210
|
+
const current = stored?.value ?? defaultSettings()
|
|
211
|
+
const next: ConciergeSettings = { ...change(current), revision: current.revision + 1, updatedAt: this.now() }
|
|
212
|
+
const put = await this.state.put(userID, CONCIERGE_COLLECTION, KEY, next, { ifVersion: stored?.version ?? 0 })
|
|
213
|
+
if (put.ok) return next
|
|
214
|
+
}
|
|
215
|
+
throw new AgentError("conflict", "The Concierge settings kept changing; retry")
|
|
216
|
+
}
|
|
217
|
+
}
|