@huynle/hub 0.7.3 → 0.8.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 +229 -0
- package/Dockerfile +7 -2
- package/README.md +153 -24
- 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/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6474 -3470
- 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/enrollment/enroll.ts +20 -8
- package/packages/cli/src/bootstrap/enrollment/policy.ts +125 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +95 -13
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +380 -9
- package/packages/cli/src/messaging.ts +3 -1
- package/packages/cli/src/setup/commands.ts +3 -2
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +403 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- 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 +96 -2
- package/packages/node/src/opencode-warm.ts +325 -0
- package/packages/node/src/run.ts +27 -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 +169 -6
- package/packages/protocol/src/launch.ts +22 -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 +3 -1
- package/packages/sdk/src/index.ts +5 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +548 -1
- 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 +215 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +17 -0
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +69 -2
- package/src/central/agent/delivery.ts +95 -15
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +5 -0
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +11 -3
- 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 +16 -2
- package/src/central/agent/launch-routes.ts +6 -1
- package/src/central/agent/launch-spec.ts +14 -1
- package/src/central/agent/message-routes.ts +2 -1
- package/src/central/agent/messages.ts +27 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/read-routes.ts +86 -19
- package/src/central/agent/routes.ts +176 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- 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 +773 -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 +339 -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 +89 -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 +130 -0
- package/src/central/concierge/proposals.ts +262 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +62 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +69 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +740 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +277 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +7 -1
- package/src/central/http.ts +633 -50
- 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 +43 -6
- 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 +772 -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 +888 -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 +97 -15
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +383 -18
- package/src/spoke/listing.ts +47 -4
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +176 -0
- package/src/spoke/spoke.ts +319 -20
- 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-BS0gVkfu.js +1 -0
- package/web/dist/assets/ArtifactPane-PLYHNs6s.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-LkDyJ5Dv.js +6 -0
- package/web/dist/assets/Library-CtxRqTjo.js +2 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/ProfilesDestination-BYiHKLmc.js +1 -0
- package/web/dist/assets/TeamsPage-BwjsAAUC.js +1 -0
- package/web/dist/assets/Watchtower-C4swrWVs.js +2 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/index-DaH5GUdV.js +31 -0
- package/web/dist/assets/index-WWuKfWxL.css +1 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/live-YyTjE47g.js +5 -0
- package/web/dist/assets/pane-layout-CKC4I00k.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-CX5j3NcD.js +1 -0
- package/web/dist/assets/renderer3d-BXIVOvfi.js +5393 -0
- package/web/dist/assets/safe-area-DfPsOdWR.js +1 -0
- package/web/dist/assets/source-CvUHTc3O.js +1 -0
- package/web/dist/assets/ui-C9Igxzjm.js +1 -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,366 @@
|
|
|
1
|
+
// Concierge (C1) shared types: one hub-managed agent per user that watches their sessions, answers status
|
|
2
|
+
// questions and acts under a grant (contract: docs/plans/2026-11-01-concierge-contracts.md).
|
|
3
|
+
|
|
4
|
+
export const CONCIERGE_ROLE = "concierge"
|
|
5
|
+
export const CONCIERGE_PARTY = "app:concierge"
|
|
6
|
+
/** OpenCode agent the plugin registers for the Concierge session (CON-04). */
|
|
7
|
+
export const CONCIERGE_AGENT = "concierge"
|
|
8
|
+
/**
|
|
9
|
+
* The only tools a Concierge session is offered (CON-04): an exact allow-list, enforced by the agent's rules and by
|
|
10
|
+
* the per-turn strip. No subagent/task, execute (Code Mode), web, shell, edit, MCP or plugin tools.
|
|
11
|
+
*/
|
|
12
|
+
export const CONCIERGE_TOOLS = ["hub", "read", "glob", "grep", "question"] as const
|
|
13
|
+
/** Spoke feature for the per-turn session context push (CON-04). */
|
|
14
|
+
export const SESSION_CONTEXT_FEATURE = "sessionContext"
|
|
15
|
+
|
|
16
|
+
export type ConciergeState = "disabled" | "starting" | "running" | "waiting_machine" | "backoff" | "failed" | "stopping"
|
|
17
|
+
|
|
18
|
+
export interface ConciergeStatus {
|
|
19
|
+
readonly state: ConciergeState
|
|
20
|
+
/** At most 200 characters, written by the hub, never model text. */
|
|
21
|
+
readonly reason?: string
|
|
22
|
+
/** The current session (running or starting). */
|
|
23
|
+
readonly sessionID?: string
|
|
24
|
+
/** Current and previous Concierge sessions (at most 20), hidden from session lists. */
|
|
25
|
+
readonly hiddenSessionIDs: readonly string[]
|
|
26
|
+
readonly launchID?: string
|
|
27
|
+
readonly machineID?: string
|
|
28
|
+
/** The @@concierge holder generation. */
|
|
29
|
+
readonly generation: number
|
|
30
|
+
readonly since: number
|
|
31
|
+
/** When the next relaunch is attempted (backoff). */
|
|
32
|
+
readonly nextAttemptAt?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Grant verbs the Concierge may hold in C1 (no launch, no roles). */
|
|
36
|
+
export type ConciergeGrantVerb = "approve" | "deny" | "answer" | "prompt" | "stop"
|
|
37
|
+
|
|
38
|
+
/** What Settings edits; it becomes a real grant whose holder is @@concierge. */
|
|
39
|
+
export interface ConciergeGrantSpec {
|
|
40
|
+
readonly allow: readonly ConciergeGrantVerb[]
|
|
41
|
+
/** 1–20 canonical project directories. */
|
|
42
|
+
readonly projects: readonly string[]
|
|
43
|
+
readonly permissions: { readonly allow: readonly string[]; readonly never: readonly string[] }
|
|
44
|
+
readonly allowAlways: boolean
|
|
45
|
+
/** "1h".."30d", default "30d". */
|
|
46
|
+
readonly expires: string
|
|
47
|
+
/** 1–1000, default 60. */
|
|
48
|
+
readonly maxActionsPerHour: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** HH:MM in an IANA time zone. */
|
|
52
|
+
export interface ConciergeQuietHours {
|
|
53
|
+
readonly start: string
|
|
54
|
+
readonly end: string
|
|
55
|
+
readonly timeZone: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** HH:MM in the settings' time zone. */
|
|
59
|
+
export interface ConciergeDigest {
|
|
60
|
+
readonly at: string
|
|
61
|
+
readonly days?: readonly (0 | 1 | 2 | 3 | 4 | 5 | 6)[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ConciergeSettings {
|
|
65
|
+
readonly enabled: boolean
|
|
66
|
+
/** "hosted" is reserved for C2. */
|
|
67
|
+
readonly where: "machine"
|
|
68
|
+
/** Required to enable. */
|
|
69
|
+
readonly machineID?: string
|
|
70
|
+
/** Absolute git repo on that machine under its node roots; a checkout, never a branch switch. */
|
|
71
|
+
readonly directory?: string
|
|
72
|
+
/** provider/model; absent means the machine default. */
|
|
73
|
+
readonly model?: string
|
|
74
|
+
/** Absent means no grant: the Concierge reads and notifies only. */
|
|
75
|
+
readonly grant?: ConciergeGrantSpec
|
|
76
|
+
/** Hub-managed, read-only to clients. */
|
|
77
|
+
readonly grantID?: string
|
|
78
|
+
readonly connectors: { readonly brain: boolean }
|
|
79
|
+
/** IANA, default from the device that enabled it. */
|
|
80
|
+
readonly timeZone: string
|
|
81
|
+
readonly quietHours?: ConciergeQuietHours
|
|
82
|
+
/** Default true: priority events still wake it during quiet hours. */
|
|
83
|
+
readonly quietAllowsUrgent: boolean
|
|
84
|
+
/** At most 4. */
|
|
85
|
+
readonly digests: readonly ConciergeDigest[]
|
|
86
|
+
readonly revision: number
|
|
87
|
+
readonly updatedAt?: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const CONCIERGE_LIMITS = {
|
|
91
|
+
hiddenHistory: 20,
|
|
92
|
+
notesBytes: 32_768,
|
|
93
|
+
digests: 4,
|
|
94
|
+
relaunchBackoffMs: [15_000, 60_000, 300_000, 900_000, 1_800_000],
|
|
95
|
+
relaunchesPerHour: 6,
|
|
96
|
+
healthyAfterMs: 600_000,
|
|
97
|
+
} as const
|
|
98
|
+
|
|
99
|
+
// ---- Session context (CON-04, spoke feature "sessionContext") ----
|
|
100
|
+
|
|
101
|
+
/** The keys of the parts a session context carries, appended to the session's system prompt in this order. */
|
|
102
|
+
export const SESSION_CONTEXT_KEYS = ["instructions", "board", "notes"] as const
|
|
103
|
+
export type SessionContextKey = (typeof SESSION_CONTEXT_KEYS)[number]
|
|
104
|
+
|
|
105
|
+
export const SESSION_CONTEXT_LIMITS = { partChars: 40_000, denyGlobs: 50, globChars: 100, allowTools: 50 } as const
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Hub → spoke: a session's per-turn context (feature "sessionContext"). The whole set replaces the previous one;
|
|
109
|
+
* `clear` drops it (no parts, tools or lock then). The spoke keeps it in memory per session and answers the
|
|
110
|
+
* revision now effective; an older revision than the effective one is refused ("stale"). `tools.allow`: the exact
|
|
111
|
+
* tool names the session keeps on every turn (every other tool, including unknown future ones, is removed: fail
|
|
112
|
+
* closed). `tools.deny`: globs of tool names removed as well. `lockAgent`: conversation mode keeps the session's
|
|
113
|
+
* agent and model and adds no orchestrator parts.
|
|
114
|
+
*/
|
|
115
|
+
export interface SessionContextRequest {
|
|
116
|
+
readonly op: "sessionContext"
|
|
117
|
+
readonly sessionID: string
|
|
118
|
+
readonly revision: number
|
|
119
|
+
readonly parts: readonly { readonly key: SessionContextKey; readonly text: string }[]
|
|
120
|
+
readonly tools?: { readonly allow?: readonly string[]; readonly deny?: readonly string[] }
|
|
121
|
+
readonly lockAgent?: boolean
|
|
122
|
+
readonly clear?: true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SessionContextResult {
|
|
126
|
+
readonly revision: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
130
|
+
const TOOL_GLOB = /^[A-Za-z0-9_.*-]+$/
|
|
131
|
+
const TOOL_NAME = /^[A-Za-z0-9_.-]+$/
|
|
132
|
+
const REQUEST_FIELDS = new Set(["op", "sessionID", "revision", "parts", "tools", "lockAgent", "clear"])
|
|
133
|
+
|
|
134
|
+
/** Why `v` is not an acceptable sessionContext request, or undefined. Exactly the known fields. */
|
|
135
|
+
export function sessionContextProblem(v: unknown): string | undefined {
|
|
136
|
+
if (!isRecord(v) || v.op !== "sessionContext") return "op must be sessionContext"
|
|
137
|
+
for (const key of Object.keys(v)) if (!REQUEST_FIELDS.has(key)) return `unknown field ${key.slice(0, 40)}`
|
|
138
|
+
if (typeof v.sessionID !== "string" || !v.sessionID || v.sessionID.length > 200) return "sessionID is required"
|
|
139
|
+
if (!Number.isSafeInteger(v.revision) || (v.revision as number) < 0) return "revision is a whole number ≥ 0"
|
|
140
|
+
if (!Array.isArray(v.parts) || v.parts.length > SESSION_CONTEXT_KEYS.length) return "parts lists at most one part per key"
|
|
141
|
+
const seen = new Set<string>()
|
|
142
|
+
for (const part of v.parts) {
|
|
143
|
+
if (!isRecord(part) || Object.keys(part).some((k) => k !== "key" && k !== "text")) return "a part is {key, text}"
|
|
144
|
+
if (!(SESSION_CONTEXT_KEYS as readonly unknown[]).includes(part.key)) return `part key is ${SESSION_CONTEXT_KEYS.join(", ")}`
|
|
145
|
+
if (seen.has(part.key as string)) return "one part per key"
|
|
146
|
+
seen.add(part.key as string)
|
|
147
|
+
if (typeof part.text !== "string" || part.text.length > SESSION_CONTEXT_LIMITS.partChars) return `part text is at most ${SESSION_CONTEXT_LIMITS.partChars} characters`
|
|
148
|
+
}
|
|
149
|
+
if (v.tools !== undefined) {
|
|
150
|
+
if (!isRecord(v.tools) || Object.keys(v.tools).some((k) => k !== "deny" && k !== "allow")) return "tools is {allow?, deny?}"
|
|
151
|
+
const { deny, allow } = v.tools
|
|
152
|
+
if (deny !== undefined) {
|
|
153
|
+
if (!Array.isArray(deny) || deny.length > SESSION_CONTEXT_LIMITS.denyGlobs) return `tools.deny lists at most ${SESSION_CONTEXT_LIMITS.denyGlobs} globs`
|
|
154
|
+
for (const glob of deny) if (typeof glob !== "string" || glob.length > SESSION_CONTEXT_LIMITS.globChars || !TOOL_GLOB.test(glob)) return "tools.deny holds tool name globs"
|
|
155
|
+
}
|
|
156
|
+
if (allow !== undefined) {
|
|
157
|
+
if (!Array.isArray(allow) || allow.length > SESSION_CONTEXT_LIMITS.allowTools) return `tools.allow lists at most ${SESSION_CONTEXT_LIMITS.allowTools} tool names`
|
|
158
|
+
for (const name of allow) if (typeof name !== "string" || name.length > SESSION_CONTEXT_LIMITS.globChars || !TOOL_NAME.test(name)) return "tools.allow holds exact tool names (no globs)"
|
|
159
|
+
}
|
|
160
|
+
if (deny === undefined && allow === undefined) return "tools is {allow?, deny?}"
|
|
161
|
+
}
|
|
162
|
+
if (v.lockAgent !== undefined && typeof v.lockAgent !== "boolean") return "lockAgent is true or false"
|
|
163
|
+
if (v.clear !== undefined) {
|
|
164
|
+
if (v.clear !== true) return "clear is true when present"
|
|
165
|
+
if (v.parts.length || v.tools !== undefined || v.lockAgent !== undefined) return "clear carries no parts, tools or lockAgent"
|
|
166
|
+
}
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Whether tool `name` matches a deny glob (`*` matches any run of characters). */
|
|
171
|
+
export function toolDenied(name: string, deny: readonly string[]): boolean {
|
|
172
|
+
return deny.some((glob) => (glob.includes("*") ? new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\-]/g, "\\$&").replace(/\*/g, ".*")}$`).test(name) : glob === name))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- Watchlist (CON-01) ----
|
|
176
|
+
|
|
177
|
+
/** What the Concierge watches: auto-active sessions, pins, explicit sessions, projects and label filters, minus mutes. */
|
|
178
|
+
export interface WatchlistConfig {
|
|
179
|
+
/** Default true: sessions working, blocked, in error, with an open blocker or Decision, or active lately. */
|
|
180
|
+
readonly autoActive: boolean
|
|
181
|
+
/** 1–24, default 8 (bounded by the 24 h journal). */
|
|
182
|
+
readonly activeWithinHours: number
|
|
183
|
+
/** Default true: the console's viewPrefs.pinnedSessions and pinnedWorkspaces. */
|
|
184
|
+
readonly usePins: boolean
|
|
185
|
+
/** Explicit ses_… IDs (at most 200): Concierge-only pins, never the console's. */
|
|
186
|
+
readonly sessions: readonly string[]
|
|
187
|
+
/** Canonical absolute project directories (at most 50). */
|
|
188
|
+
readonly projects: readonly string[]
|
|
189
|
+
/** At most 20 filters; one matches a session when all its pairs equal its launch's labels. */
|
|
190
|
+
readonly labels: readonly Readonly<Record<string, string>>[]
|
|
191
|
+
/** ses_… IDs (at most 500); beats every include. */
|
|
192
|
+
readonly muted: readonly string[]
|
|
193
|
+
/** Canonical absolute project directories (at most 50); beats every include. */
|
|
194
|
+
readonly mutedProjects: readonly string[]
|
|
195
|
+
readonly revision: number
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type WatchReason = "active" | "pinned" | "pinned_project" | "explicit" | "project" | "label"
|
|
199
|
+
|
|
200
|
+
export interface WatchlistEntry {
|
|
201
|
+
readonly sessionID: string
|
|
202
|
+
readonly reasons: readonly WatchReason[]
|
|
203
|
+
readonly lastActiveAt?: number
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** GET /api/agent/concierge/watchlist. */
|
|
207
|
+
export interface WatchlistView {
|
|
208
|
+
readonly config: WatchlistConfig
|
|
209
|
+
readonly entries: readonly WatchlistEntry[]
|
|
210
|
+
readonly truncated: boolean
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const WATCHLIST_LIMITS = { entries: 200, sessions: 200, projects: 50, labels: 20, muted: 500 } as const
|
|
214
|
+
|
|
215
|
+
export const WATCHLIST_DEFAULTS = { autoActive: true, activeWithinHours: 8, usePins: true } as const
|
|
216
|
+
|
|
217
|
+
// ---- Session intent (CON-02) ----
|
|
218
|
+
|
|
219
|
+
/** Who set a session's intent, highest precedence first: user > launch > self > prompt > title. */
|
|
220
|
+
export type IntentSource = "user" | "launch" | "self" | "prompt" | "title"
|
|
221
|
+
|
|
222
|
+
/** What a session is for: untrusted text (sanitised, one line), never interpreted as instructions. */
|
|
223
|
+
export interface SessionIntent {
|
|
224
|
+
readonly sessionID: string
|
|
225
|
+
/** Sanitised, one line, 1–280 characters (INTENT_MAX). */
|
|
226
|
+
readonly text: string
|
|
227
|
+
readonly source: IntentSource
|
|
228
|
+
/** 0 before the hub stored anything for the session (a title fallback). */
|
|
229
|
+
readonly revision: number
|
|
230
|
+
readonly updatedAt: number
|
|
231
|
+
/** Party key of the writer (user, app:…, ses_…); absent for derived intents. */
|
|
232
|
+
readonly by?: string
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const INTENT_MAX = 280
|
|
236
|
+
|
|
237
|
+
// ---- Situation board (CON-03) ----
|
|
238
|
+
|
|
239
|
+
/** A board section, as on the workspace board: ok, omitted (scope), unsupported (not wired) or failed (its source). */
|
|
240
|
+
export type ConciergeBoardSection<T> =
|
|
241
|
+
| { readonly status: "ok"; readonly items: readonly T[]; readonly truncated: boolean }
|
|
242
|
+
| { readonly status: "omitted"; readonly reason: "scope"; readonly scope: string }
|
|
243
|
+
| { readonly status: "unsupported"; readonly feature: string }
|
|
244
|
+
| { readonly status: "failed" }
|
|
245
|
+
|
|
246
|
+
export type ConciergeBoardState = "idle" | "working" | "blocked" | "error" | "offline"
|
|
247
|
+
|
|
248
|
+
export type ConciergeResultOutcome = "turn_done" | "error" | "launch_failed" | "launch_needs_user" | "launch_idle"
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* (CON-13) A pending blocker the Concierge may act on, by ID (at most CONCIERGE_BOARD_PENDING_MAX per session): a
|
|
252
|
+
* permission with its action (an enum-like label, "other" otherwise) and `inGrant`, which the hub computes with the
|
|
253
|
+
* acting engine's own check (would approve once succeed under the @@concierge grant): the model never decides it;
|
|
254
|
+
* `auto` (CON-13 decision): inGrant and not the tap risk tier, the only ones the Concierge approves itself. A
|
|
255
|
+
* form with its ID (concierge_form reads its fields). Never summaries, commands or paths.
|
|
256
|
+
*/
|
|
257
|
+
export type ConciergeBoardBlocker =
|
|
258
|
+
| { readonly kind: "permission"; readonly id: string; readonly action: string; readonly inGrant: boolean; readonly auto: boolean }
|
|
259
|
+
| { readonly kind: "form"; readonly id: string }
|
|
260
|
+
|
|
261
|
+
export const CONCIERGE_BOARD_PENDING_MAX = 6
|
|
262
|
+
|
|
263
|
+
/** One watched root session: IDs, enums, counts and times; `intent`, `title`, `machine`, `project` are untrusted. */
|
|
264
|
+
export interface ConciergeBoardSession {
|
|
265
|
+
readonly sessionID: string
|
|
266
|
+
/** The @name slug (with "@"). */
|
|
267
|
+
readonly name?: string
|
|
268
|
+
/** Project label (lowercased slug, as the directory has it). */
|
|
269
|
+
readonly project?: string
|
|
270
|
+
/** Machine name (sanitised label). */
|
|
271
|
+
readonly machine?: string
|
|
272
|
+
readonly state: ConciergeBoardState
|
|
273
|
+
/** When the current state began, as far as the hub knows (activity, last result, or lastSeen when offline). */
|
|
274
|
+
readonly since?: number
|
|
275
|
+
/** reported: false when this hub's catalog does not report the session now (never "none"). */
|
|
276
|
+
readonly blockers: { readonly reported: false } | { readonly reported: true; readonly count: number; readonly kinds: readonly string[]; readonly pending?: readonly ConciergeBoardBlocker[] }
|
|
277
|
+
/** Open Decision IDs about this tree (at most 8). */
|
|
278
|
+
readonly decisions: readonly string[]
|
|
279
|
+
/** The stored intent (at most 280 characters), untrusted. */
|
|
280
|
+
readonly intent?: { readonly text: string; readonly source: IntentSource }
|
|
281
|
+
/** The non-placeholder title (at most 80 characters), untrusted; only when there is no intent. */
|
|
282
|
+
readonly title?: string
|
|
283
|
+
readonly lastResult?: { readonly at: number; readonly outcome: ConciergeResultOutcome }
|
|
284
|
+
readonly reasons: readonly WatchReason[]
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface ConciergeBoardDecision {
|
|
288
|
+
readonly id: string
|
|
289
|
+
/** Party label (sanitised; "@@concierge" for the Concierge's own sessions). */
|
|
290
|
+
readonly from: string
|
|
291
|
+
/** Session IDs it concerns. */
|
|
292
|
+
readonly about: readonly string[]
|
|
293
|
+
readonly urgency: "low" | "normal" | "high"
|
|
294
|
+
readonly tier: "confirm" | "tap"
|
|
295
|
+
readonly at: number
|
|
296
|
+
readonly deadline?: number
|
|
297
|
+
/** Under discussion by voice (CON-06). */
|
|
298
|
+
readonly armed?: true
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface ConciergeBoardMachine {
|
|
302
|
+
readonly id: string
|
|
303
|
+
readonly name: string
|
|
304
|
+
readonly online: boolean
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface ConciergeBoard {
|
|
308
|
+
/** Monotonic per user; changes only when the content (everything but `at`) changes. */
|
|
309
|
+
readonly revision: number
|
|
310
|
+
readonly at: number
|
|
311
|
+
/** At most CONCIERGE_BOARD_LIMITS.sessions, in watchlist order. */
|
|
312
|
+
readonly sessions: ConciergeBoardSection<ConciergeBoardSession>
|
|
313
|
+
/** Open, at most CONCIERGE_BOARD_LIMITS.decisions, newest first. */
|
|
314
|
+
readonly decisions: ConciergeBoardSection<ConciergeBoardDecision>
|
|
315
|
+
readonly machines: ConciergeBoardSection<ConciergeBoardMachine>
|
|
316
|
+
readonly counts: { readonly watched: number; readonly working: number; readonly blocked: number; readonly error: number; readonly openDecisions: number }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** GET /api/agent/concierge/board. */
|
|
320
|
+
export interface ConciergeBoardView {
|
|
321
|
+
readonly board: ConciergeBoard
|
|
322
|
+
/** renderBoard(board): the text the Concierge gets, at most CONCIERGE_BOARD_LIMITS.renderedChars. */
|
|
323
|
+
readonly rendered: string
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const CONCIERGE_BOARD_LIMITS = { sessions: 40, decisions: 20, renderedChars: 12_000, titleChars: 80, decisionsPerSession: 8, machines: 50 } as const
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
// ---- Event-driven turns (CON-05) ----
|
|
330
|
+
|
|
331
|
+
// ---- Approval tiers (CON-06) ----
|
|
332
|
+
|
|
333
|
+
/** The verbs a Concierge proposal covers (POST /api/agent/concierge/proposals). */
|
|
334
|
+
export type ConciergeProposalVerb = "stop" | "deny" | "prompt" | "answer"
|
|
335
|
+
|
|
336
|
+
/** POST /api/agent/concierge/proposals (the current Concierge's act token only) → 201 {decision}. */
|
|
337
|
+
export interface ConciergeProposalBody {
|
|
338
|
+
readonly verb: ConciergeProposalVerb
|
|
339
|
+
/** @name, @@role, ses_… (resolved for the user). */
|
|
340
|
+
readonly session: string
|
|
341
|
+
/** stop/deny: none; prompt: {text}; answer: {answer} | {cancel: true}. */
|
|
342
|
+
readonly input?: { readonly text?: string; readonly answer?: Readonly<Record<string, string | number | boolean | readonly string[]>>; readonly cancel?: true }
|
|
343
|
+
/** The Concierge's reason (≤ whyChars): the recommendation's with `recommend`, else shown after the question. */
|
|
344
|
+
readonly why?: string
|
|
345
|
+
/** (CON-09a) The Concierge recommends Do it. Absent/false: no recommendation (the user only asked it to propose). */
|
|
346
|
+
readonly recommend?: boolean
|
|
347
|
+
/** deny only. */
|
|
348
|
+
readonly requestID?: string
|
|
349
|
+
/** answer only. */
|
|
350
|
+
readonly formID?: string
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The hub's tier per proposed verb (never the Concierge's): free text only by a tap. */
|
|
354
|
+
export const CONCIERGE_PROPOSAL_TIERS = { stop: "confirm", deny: "confirm", prompt: "tap", answer: "tap" } as const satisfies Record<ConciergeProposalVerb, "confirm" | "tap">
|
|
355
|
+
|
|
356
|
+
/** POST/GET /api/agent/concierge/arm → {armed: ConciergeArmed | null}; SSE {type: "concierge", armed}. */
|
|
357
|
+
export interface ConciergeArmed {
|
|
358
|
+
readonly decisionID: string
|
|
359
|
+
readonly at: number
|
|
360
|
+
readonly expiresAt: number
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** How the user answered a Decision (POST /api/decisions/:id/answer `via`; absent = tap). */
|
|
364
|
+
export type DecisionAnswerVia = "tap" | "voice" | "text"
|
|
365
|
+
|
|
366
|
+
export const CONCIERGE_TIER_LIMITS = { proposalsOpen: 10, proposalsPerHour: 30, armedTtlMs: 120_000, promptChars: 8_000, answerJsonChars: 4_000, whyChars: 1_000 } as const
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Shared vocabulary of the workspaces/artifacts/teams contracts (HWA-02, docs/plans/2026-10-30-hub-workspaces-
|
|
2
|
+
// artifacts-contracts.md). Pure: IDs, slugs, bounded text and the expected-revision / idempotency rules every new
|
|
3
|
+
// record follows. Domain modules (hub-workspace.ts, profiles.ts, artifacts.ts, teams.ts, graph.ts) build on it.
|
|
4
|
+
import { UNSAFE_CHARS } from "./wire.ts"
|
|
5
|
+
|
|
6
|
+
/** Record-schema version of every new record; readers reject other versions rather than guess. */
|
|
7
|
+
export const CONTRACT_VERSION = 1
|
|
8
|
+
|
|
9
|
+
/** ID prefixes of the new records. An ID is `<prefix>_<16–32 base32/alnum chars>`, minted by the hub. */
|
|
10
|
+
export const ID_PREFIXES = {
|
|
11
|
+
workspace: "ws",
|
|
12
|
+
binding: "wsb",
|
|
13
|
+
member: "wsm",
|
|
14
|
+
assignment: "asg",
|
|
15
|
+
supervision: "sup",
|
|
16
|
+
checkpoint: "chk",
|
|
17
|
+
sideChat: "chat",
|
|
18
|
+
profile: "prof",
|
|
19
|
+
artifact: "art",
|
|
20
|
+
revision: "rev",
|
|
21
|
+
blob: "blob",
|
|
22
|
+
aiJob: "job",
|
|
23
|
+
team: "team",
|
|
24
|
+
staffing: "stf",
|
|
25
|
+
intake: "itk",
|
|
26
|
+
} as const
|
|
27
|
+
export type IdKind = keyof typeof ID_PREFIXES
|
|
28
|
+
|
|
29
|
+
const ID_BODY = /^[A-Za-z0-9]{16,32}$/
|
|
30
|
+
|
|
31
|
+
/** True when `v` is a well-formed ID of that kind. */
|
|
32
|
+
export function isContractID(kind: IdKind, v: unknown): v is string {
|
|
33
|
+
if (typeof v !== "string") return false
|
|
34
|
+
const prefix = `${ID_PREFIXES[kind]}_`
|
|
35
|
+
return v.startsWith(prefix) && ID_BODY.test(v.slice(prefix.length))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Slugs (team names, role names, profile names): the @name syntax, lowercase letters, digits, dashes, 2–40. */
|
|
39
|
+
export const SLUG = /^[a-z0-9-]{2,40}$/
|
|
40
|
+
export const isSlug = (v: unknown): v is string => typeof v === "string" && SLUG.test(v)
|
|
41
|
+
|
|
42
|
+
/** One-line labels (titles, role labels): 1–120 characters, no control or format characters. */
|
|
43
|
+
export const LABEL_MAX = 120
|
|
44
|
+
export const isLabel = (v: unknown, max = LABEL_MAX): v is string =>
|
|
45
|
+
typeof v === "string" && v.trim().length > 0 && v.length <= max && !UNSAFE_CHARS.test(v)
|
|
46
|
+
|
|
47
|
+
/** Free-form descriptions/instructions: up to `max` characters (newlines allowed, other controls not). */
|
|
48
|
+
export const DESCRIPTION_MAX = 4_000
|
|
49
|
+
const UNSAFE_MULTILINE = /[\u0000-\u0009\u000b-\u001f\u007f\u2028\u2029\u200b-\u200f\u202a-\u202e\u2066-\u2069]/
|
|
50
|
+
export const isText = (v: unknown, max = DESCRIPTION_MAX): v is string => typeof v === "string" && v.length <= max && !UNSAFE_MULTILINE.test(v)
|
|
51
|
+
|
|
52
|
+
/** Tags and metadata (teams, artifacts, workspaces): bounded, filterable, never secrets. */
|
|
53
|
+
export const TAGS_MAX = 32
|
|
54
|
+
export const METADATA_MAX_KEYS = 32
|
|
55
|
+
export const METADATA_VALUE_MAX = 200
|
|
56
|
+
export function tagsProblem(v: unknown): string | undefined {
|
|
57
|
+
if (v === undefined) return undefined
|
|
58
|
+
if (!Array.isArray(v) || v.length > TAGS_MAX) return `tags must be a list of at most ${TAGS_MAX}`
|
|
59
|
+
if (!v.every(isSlug)) return "each tag must be 2–40 lowercase letters, digits or dashes"
|
|
60
|
+
if (new Set(v).size !== v.length) return "tags must be unique"
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
export function metadataProblem(v: unknown): string | undefined {
|
|
64
|
+
if (v === undefined) return undefined
|
|
65
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return "metadata must be an object"
|
|
66
|
+
const entries = Object.entries(v)
|
|
67
|
+
if (entries.length > METADATA_MAX_KEYS) return `metadata has more than ${METADATA_MAX_KEYS} keys`
|
|
68
|
+
for (const [key, value] of entries) {
|
|
69
|
+
if (!isSlug(key)) return `metadata key ${JSON.stringify(String(key).slice(0, 50))} must be a slug`
|
|
70
|
+
if (!isLabel(value, METADATA_VALUE_MAX)) return `metadata.${key} must be 1–${METADATA_VALUE_MAX} characters of text`
|
|
71
|
+
}
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Optimistic concurrency for every mutable record: each record carries `revision` (1 on create, +1 per change).
|
|
77
|
+
* A mutation names `expectedRevision`; a mismatch is answered 409 `conflict` with `extra.currentRevision`.
|
|
78
|
+
* Creates take `expectedRevision: 0` implicitly. Revisions are never reused, even after delete.
|
|
79
|
+
*/
|
|
80
|
+
export interface Revisioned {
|
|
81
|
+
readonly revision: number
|
|
82
|
+
}
|
|
83
|
+
export const isExpectedRevision = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v >= 0
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Idempotency: creating mutations (workspace, artifact, revision, AI job, intake, staffing launch) accept an
|
|
87
|
+
* `Idempotency-Key` header (1–200 of [A-Za-z0-9_.:-]); the same key + same body within IDEMPOTENCY_TTL_MS returns
|
|
88
|
+
* the original result, a different body under the same key is 409 `conflict`. Keys are per user and per route.
|
|
89
|
+
*/
|
|
90
|
+
export const IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,200}$/
|
|
91
|
+
export const IDEMPOTENCY_TTL_MS = 24 * 60 * 60_000
|
|
92
|
+
|
|
93
|
+
/** Record lifecycle shared by workspaces, teams, profiles and artifacts: archive is reversible, delete is not. */
|
|
94
|
+
export const RECORD_STATES = ["active", "archived"] as const
|
|
95
|
+
export type RecordState = (typeof RECORD_STATES)[number]
|
|
96
|
+
|
|
97
|
+
/** Bounded list reads: `limit` 1–LIST_LIMIT_MAX (default LIST_LIMIT_DEFAULT) and an opaque `cursor`. */
|
|
98
|
+
export const LIST_LIMIT_DEFAULT = 50
|
|
99
|
+
export const LIST_LIMIT_MAX = 200
|
|
100
|
+
export interface Page<T> {
|
|
101
|
+
readonly items: readonly T[]
|
|
102
|
+
/** Absent on the last page. */
|
|
103
|
+
readonly cursor?: string
|
|
104
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Interaction graph projection for the Watchtower (HWA-02 contract; API HWA-31, 3D page HWA-32, 2D/teams
|
|
2
|
+
// HWA-38/39). Decisions 5e4l4u8v (page), 50slcj1x (3d-force-graph + force-graph).
|
|
3
|
+
//
|
|
4
|
+
// GET /api/agent/graph?since=&until=&machine=&harness=&team=&tag=&project=&limit= returns a bounded snapshot over
|
|
5
|
+
// the existing registry, message store, Decisions and canonical events. It carries counts and IDs, never message
|
|
6
|
+
// text: edge detail is read through the existing authorized message reads. Quiet machines and sessions are
|
|
7
|
+
// included. Live updates arrive as compact `graph.invalidated` hints; the client re-fetches.
|
|
8
|
+
//
|
|
9
|
+
// History is bounded by message retention (200 per party; 10,000 / 20 MiB per user): `retention.truncated`
|
|
10
|
+
// tells the UI to disclose that older interactions were evicted.
|
|
11
|
+
|
|
12
|
+
export const GRAPH_LIMITS = {
|
|
13
|
+
nodes: 2_000,
|
|
14
|
+
edges: 5_000,
|
|
15
|
+
/** Longest window a snapshot may cover. */
|
|
16
|
+
windowMs: 7 * 24 * 60 * 60_000,
|
|
17
|
+
defaultWindowMs: 24 * 60 * 60_000,
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
export type GraphNodeKind = "hub" | "machine" | "session" | "user" | "team"
|
|
21
|
+
|
|
22
|
+
export interface GraphNode {
|
|
23
|
+
readonly id: string
|
|
24
|
+
readonly kind: GraphNodeKind
|
|
25
|
+
/** Display label: an @name, machine name or team title. Never a prompt or message. */
|
|
26
|
+
readonly label: string
|
|
27
|
+
readonly machineID?: string
|
|
28
|
+
/**
|
|
29
|
+
* The harness kind: the reporting process's, else (offline) the last one the hub saw for the session, else its
|
|
30
|
+
* launch's, else its machine's when the user's processes there all run one harness (RF-02). Absent: unknown.
|
|
31
|
+
*/
|
|
32
|
+
readonly harness?: string
|
|
33
|
+
/** Native ancestry (parentID); distinct from supervision. */
|
|
34
|
+
readonly parentID?: string
|
|
35
|
+
readonly supervisorID?: string
|
|
36
|
+
readonly teamIDs?: readonly string[]
|
|
37
|
+
readonly status?: "idle" | "working" | "blocked" | "error" | "offline" | "ended"
|
|
38
|
+
readonly tags?: readonly string[]
|
|
39
|
+
readonly project?: string
|
|
40
|
+
/** (CON-04) The user's Concierge session (current or previous): hidden from session lists, shown here. */
|
|
41
|
+
readonly concierge?: true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GraphEdge {
|
|
45
|
+
readonly id: string
|
|
46
|
+
readonly from: string
|
|
47
|
+
readonly to: string
|
|
48
|
+
readonly kind: "msg" | "ask" | "reply" | "decision" | "launch" | "policy"
|
|
49
|
+
readonly count: number
|
|
50
|
+
readonly lastAt: number
|
|
51
|
+
/** For kind "policy": a permitted team path that may have no traffic (policy-vs-actual overlay). */
|
|
52
|
+
readonly teamID?: string
|
|
53
|
+
readonly denied?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GraphSnapshot {
|
|
57
|
+
readonly v: 1
|
|
58
|
+
readonly at: number
|
|
59
|
+
readonly window: { readonly since: number; readonly until: number }
|
|
60
|
+
readonly nodes: readonly GraphNode[]
|
|
61
|
+
readonly edges: readonly GraphEdge[]
|
|
62
|
+
readonly retention: { readonly truncated: boolean; readonly oldestAt?: number }
|
|
63
|
+
/** True when limits cut the snapshot (narrow the filters). */
|
|
64
|
+
readonly capped: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Budget for the lazily loaded Watchtower chunk (three + 3d-force-graph + force-graph), gzip bytes. */
|
|
68
|
+
export const WATCHTOWER_CHUNK_BUDGET = 700 * 1024
|
|
69
|
+
/** @deprecated The page was renamed Watchtower; kept for API stability. Use WATCHTOWER_CHUNK_BUDGET. */
|
|
70
|
+
export const BIRDS_EYE_CHUNK_BUDGET = WATCHTOWER_CHUNK_BUDGET
|