wicked-crew 0.10.2 → 0.12.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.
Files changed (106) hide show
  1. package/dist/api/capture.d.ts +2 -2
  2. package/dist/api/delivery-assurance.d.ts +1 -6
  3. package/dist/api/delivery-assurance.d.ts.map +1 -1
  4. package/dist/api/delivery-assurance.js +4 -14
  5. package/dist/api/delivery-assurance.js.map +1 -1
  6. package/dist/api/governance-steering.d.ts +2 -2
  7. package/dist/api/mcp.d.ts +4 -4
  8. package/dist/api/recording.d.ts +29 -3
  9. package/dist/api/recording.d.ts.map +1 -1
  10. package/dist/api/recording.js +188 -0
  11. package/dist/api/recording.js.map +1 -1
  12. package/dist/api/roster-standing.d.ts +6 -2
  13. package/dist/api/roster-standing.d.ts.map +1 -1
  14. package/dist/api/roster-standing.js +62 -13
  15. package/dist/api/roster-standing.js.map +1 -1
  16. package/dist/api/routes.d.ts +115 -112
  17. package/dist/api/routes.d.ts.map +1 -1
  18. package/dist/api/routes.js +227 -6
  19. package/dist/api/routes.js.map +1 -1
  20. package/dist/api/seat-container-login.d.ts +60 -0
  21. package/dist/api/seat-container-login.d.ts.map +1 -0
  22. package/dist/api/seat-container-login.js +158 -0
  23. package/dist/api/seat-container-login.js.map +1 -0
  24. package/dist/api/seat-probe.d.ts +7 -0
  25. package/dist/api/seat-probe.d.ts.map +1 -1
  26. package/dist/api/seat-probe.js +27 -5
  27. package/dist/api/seat-probe.js.map +1 -1
  28. package/dist/api/seat-version.d.ts +8 -0
  29. package/dist/api/seat-version.d.ts.map +1 -1
  30. package/dist/api/seat-version.js +36 -4
  31. package/dist/api/seat-version.js.map +1 -1
  32. package/dist/api/server.d.ts.map +1 -1
  33. package/dist/api/server.js +4 -0
  34. package/dist/api/server.js.map +1 -1
  35. package/dist/api/testing.d.ts +4 -4
  36. package/dist/appearance/video-palette.d.ts +14 -0
  37. package/dist/appearance/video-palette.d.ts.map +1 -0
  38. package/dist/appearance/video-palette.js +75 -0
  39. package/dist/appearance/video-palette.js.map +1 -0
  40. package/dist/campaigns/routes.d.ts +2 -2
  41. package/dist/core/adapter.d.ts.map +1 -1
  42. package/dist/core/adapter.js +20 -0
  43. package/dist/core/adapter.js.map +1 -1
  44. package/dist/core/builtin-catalog.d.ts.map +1 -1
  45. package/dist/core/builtin-catalog.js +6 -1
  46. package/dist/core/builtin-catalog.js.map +1 -1
  47. package/dist/core/engine-roster.d.ts.map +1 -1
  48. package/dist/core/engine-roster.js +1 -0
  49. package/dist/core/engine-roster.js.map +1 -1
  50. package/dist/core/qe-acceptance-setting.d.ts +65 -0
  51. package/dist/core/qe-acceptance-setting.d.ts.map +1 -0
  52. package/dist/core/qe-acceptance-setting.js +93 -0
  53. package/dist/core/qe-acceptance-setting.js.map +1 -0
  54. package/dist/core/run-identity.d.ts.map +1 -1
  55. package/dist/core/run-identity.js +3 -2
  56. package/dist/core/run-identity.js.map +1 -1
  57. package/dist/core/types.d.ts.map +1 -1
  58. package/dist/core/types.js +3 -0
  59. package/dist/core/types.js.map +1 -1
  60. package/dist/editors/pack-install.d.ts +36 -3
  61. package/dist/editors/pack-install.d.ts.map +1 -1
  62. package/dist/editors/pack-install.js +71 -6
  63. package/dist/editors/pack-install.js.map +1 -1
  64. package/dist/editors/registry.d.ts +153 -14
  65. package/dist/editors/registry.d.ts.map +1 -1
  66. package/dist/editors/registry.js +29 -4
  67. package/dist/editors/registry.js.map +1 -1
  68. package/dist/editors/surfaces.d.ts +48 -0
  69. package/dist/editors/surfaces.d.ts.map +1 -0
  70. package/dist/editors/surfaces.js +294 -0
  71. package/dist/editors/surfaces.js.map +1 -0
  72. package/dist/events/terminals.d.ts +42 -2
  73. package/dist/events/terminals.d.ts.map +1 -1
  74. package/dist/events/terminals.js +136 -3
  75. package/dist/events/terminals.js.map +1 -1
  76. package/dist/interactive/chat-events.d.ts +4 -3
  77. package/dist/interactive/chat-events.d.ts.map +1 -1
  78. package/dist/interactive/chat-events.js +7 -6
  79. package/dist/interactive/chat-events.js.map +1 -1
  80. package/dist/interactive/doc-grounding.d.ts.map +1 -1
  81. package/dist/interactive/doc-grounding.js +7 -1
  82. package/dist/interactive/doc-grounding.js.map +1 -1
  83. package/dist/interactive/draft-skill.d.ts +11 -4
  84. package/dist/interactive/draft-skill.d.ts.map +1 -1
  85. package/dist/interactive/draft-skill.js +42 -9
  86. package/dist/interactive/draft-skill.js.map +1 -1
  87. package/dist/projects/state-home-registry.d.ts.map +1 -1
  88. package/dist/projects/state-home-registry.js +2 -0
  89. package/dist/projects/state-home-registry.js.map +1 -1
  90. package/dist/studio/assets/index-1Ucvzihb.js +770 -0
  91. package/dist/studio/assets/index-B0u_f6HD.css +32 -0
  92. package/dist/studio/editors/wicked-deck/editor.json +88 -0
  93. package/dist/studio/editors/wicked-deck/index.html +230 -0
  94. package/dist/studio/editors/wicked-doc/editor.json +91 -0
  95. package/dist/studio/editors/wicked-doc/index.html +161 -0
  96. package/dist/studio/editors/wicked-page/editor.json +42 -4
  97. package/dist/studio/editors/wicked-page/index.html +25 -8
  98. package/dist/studio/editors/wicked-video/editor.json +271 -0
  99. package/dist/studio/editors/wicked-video/index.html +28 -0
  100. package/dist/studio/index.html +2 -2
  101. package/dist/studio/testid-inventory.json +3844 -1364
  102. package/dist/team/routes.d.ts +2 -2
  103. package/endpoint-manifest.json +56 -1
  104. package/package.json +3 -3
  105. package/dist/studio/assets/index-C6DLorst.css +0 -32
  106. package/dist/studio/assets/index-CAhuFWeA.js +0 -609
@@ -8,6 +8,7 @@ import { readFileSync, existsSync } from 'node:fs';
8
8
  import { promises as fsp } from 'node:fs';
9
9
  import { isAbsolute, join, relative, resolve } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { videoPalette } from '../appearance/video-palette.js';
11
12
  import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, addonSupportsQeOverride, addonSupportsReducedAssurance, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
12
13
  import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
13
14
  import { resolveCursorUnit } from '../core/cursor.js';
@@ -25,6 +26,7 @@ import { defaultLaunchRoster, markOptIn } from '../core/opt-in-seats.js';
25
26
  import { chatSeatAdmission } from './seat-standing.js';
26
27
  import { seatGovernanceDiagnostics, seatGovernanceWarnings, } from './seat-governance.js';
27
28
  import { ROSTER_REFRESH_WAIT_MS, rosterWithStandingFactory } from './roster-standing.js';
29
+ import { SeatContainerStatus, isContainerLoginCli, isContainerSeatFloor, loginDetail, runSeatLogin, seatLoginDepsFor, } from './seat-container-login.js';
28
30
  import { ChatTurnIndex } from './chat-turns.js';
29
31
  import { ChatScopeIndex, chatScopeDeps, chatScopeStatement, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
30
32
  import { ASK_TURN_BUDGET_SECS, AskPathIndex, answerStep, isAskTurn } from './ask-paths.js';
@@ -46,7 +48,8 @@ import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js
46
48
  import { inventoryComplete, parseInventoryClaims } from '../core/inventory-claim.js';
47
49
  import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
48
50
  import { registerTestingRoutes } from './testing.js';
49
- import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
51
+ import { DEMO_PRESET, demoLaunchRoots, registerDemoDocumentRoute, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
52
+ import { projectDocsRoot } from '../interactive/project-root.js';
50
53
  import { registerSkillsRoutes } from './skills.js';
51
54
  import { registerMcpRoutes } from './mcp.js';
52
55
  import { McpPolicies } from '../mcp/policies.js';
@@ -57,7 +60,8 @@ import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../s
57
60
  import { REQUIRED_GARDEN_VERSION } from '../skills/plugin-source.js';
58
61
  import { engineRosterJson, noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
59
62
  import { launchAssuranceNotice } from '../core/assurance.js';
60
- import { QE_ACCEPTANCE, QE_PRESETS, aggregateReceipt, postHocDeliveryAssurance, qeAcceptanceFromView, qeAcceptanceRefusal, receiptOf, qeAcceptanceNotRequired, qeDecisionBypasses, qeDecisionOf, unreadableQeAcceptance, } from './delivery-assurance.js';
63
+ import { launchRequiresQe, mayRequireQe, qeAcceptanceMode, qeSettingSkipReason } from '../core/qe-acceptance-setting.js';
64
+ import { aggregateReceipt, postHocDeliveryAssurance, qeAcceptanceFromView, qeAcceptanceRefusal, receiptOf, qeAcceptanceNotRequired, qeDecisionBypasses, qeDecisionOf, unreadableQeAcceptance, } from './delivery-assurance.js';
61
65
  import { ProjectSettingsStore } from '../projects/settings.js';
62
66
  import { isHumanOperator } from './watch-routes.js';
63
67
  import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
@@ -1077,6 +1081,9 @@ runtime = {}) {
1077
1081
  // spellings where the contract has no name, statusCodes list every code the route answers on
1078
1082
  // purpose. Declared on the highest-traffic run-lifecycle routes first; the manifest records
1079
1083
  // null / [] for the rest ("where declared", never invented). See src/api/endpoint-manifest.ts.
1084
+ // The daemon's QE acceptance setting as read now ('required' when the settings cannot be read:
1085
+ // fail closed, never off by accident).
1086
+ const qeAcceptanceNow = async () => qeAcceptanceMode(await Promise.resolve().then(() => adapter.getSettings()).catch(() => null));
1080
1087
  app.get(`${V}/health`, { config: { manifest: { statusCodes: [200] } } }, async () => {
1081
1088
  const ping = await enginePing();
1082
1089
  // F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
@@ -1103,6 +1110,9 @@ runtime = {}) {
1103
1110
  // QE-IN-APP-WORKFLOWS: `LaunchRunBody.skipQeAcceptance` / `forceQeAcceptance` reach an addon
1104
1111
  // that carries the QE decision; never an invented capability.
1105
1112
  qeAcceptanceOverride: typeof adapter.engineCapabilities === 'function' && addonSupportsQeOverride(),
1113
+ // api-types 0.120.0: the daemon's QE acceptance setting, read per probe ('off' = every launch
1114
+ // that requires QE acceptance skips it, labelled with the setting).
1115
+ qeAcceptance: await qeAcceptanceNow(),
1106
1116
  };
1107
1117
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
1108
1118
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
@@ -1267,7 +1277,9 @@ runtime = {}) {
1267
1277
  // (built over the same `seatHealth`); a directly-driven route set builds an equivalent one here.
1268
1278
  // `POST /chats` seats its defaults through the SAME standing, so the roster and the chat never
1269
1279
  // disagree about a seat.
1270
- const rosterWithStanding = runtime.rosterWithStanding ?? rosterWithStandingFactory({ seatHealth, signedIn });
1280
+ const seatLoginDeps = seatLoginDepsFor(adapter.dbPath, runtime.seatLoginExec);
1281
+ const containerStatus = runtime.rosterWithStanding?.containerStatus ?? new SeatContainerStatus(seatLoginDeps);
1282
+ const rosterWithStanding = runtime.rosterWithStanding ?? rosterWithStandingFactory({ seatHealth, signedIn, containerStatus });
1271
1283
  // A3b (DES-seats-page): `?refresh=<key>` re-runs that seat's sign-in check now and waits for it,
1272
1284
  // bounded, so the Agents page's "I've signed in: check again" reads the seat's fresh answer, not
1273
1285
  // the probe cache's 30-second-old one. A read with no parameter never waits.
@@ -1819,15 +1831,23 @@ runtime = {}) {
1819
1831
  // F-E2E-030: only the explicit `'auto'` opts out of the engine's deliver gate. `'human'` and
1820
1832
  // an omitted field both leave `autoDeliver` off the input — the gate is the engine's default,
1821
1833
  // so the wire never has to say "gate me" to be gated.
1834
+ // The daemon's QE acceptance setting, read ONCE for this launch: the deliverGate check below and
1835
+ // the skip handed to the engine come from the same reading (core/qe-acceptance-setting.ts).
1836
+ // An addon without the QE decision enforces none, so the setting has nothing to skip on it.
1837
+ const qeMode = addonSupportsQeOverride() ? await qeAcceptanceNow() : 'required';
1838
+ const qeRequired = b.deliverGate === 'auto' || qeMode === 'off' ? await launchRequiresQe(adapter, b.workflow, b.projectId) : false;
1839
+ const settingSkip = qeSettingSkipReason(qeMode, { skipQeAcceptanceReason: b.skipQeAcceptance?.reason, forceQeAcceptance: b.forceQeAcceptance }, qeRequired);
1822
1840
  if (b.deliverGate === 'auto') {
1823
1841
  // wicked-core#850 EX-03: a workflow that requires QE acceptance delivers only through the deliver
1824
1842
  // gate, where crew checks the verdict. Unattended delivery would push before anyone could.
1825
1843
  // QE-IN-APP-WORKFLOWS: an explicit operator skip leaves no verdict to check, so it may. A name
1826
1844
  // the engine resolves to a built-in preset that requires QE acceptance is judged by that preset
1827
1845
  // (the engine resolves a preset before any def, so a def of the same name never speaks for it).
1828
- const required = b.workflow !== undefined ? adapter.getWorkflow(b.workflow)?.required_instruments : undefined;
1829
- const qeRequired = required?.includes(QE_ACCEPTANCE) === true || (b.workflow !== undefined && QE_PRESETS.has(b.workflow));
1830
- if (qeRequired && b.skipQeAcceptance === undefined) {
1846
+ // The daemon's `qeAcceptance: 'off'` setting skips it the same way (resolved once, above, and
1847
+ // handed to the engine with this launch), unless this launch forces QE acceptance.
1848
+ // Refused on the CONSERVATIVE reading (a built-in QE name or a QE def counts even when a project
1849
+ // preset shadows it now): only an explicit or setting skip lets QE work deliver unattended.
1850
+ if (mayRequireQe(adapter, b.workflow, qeRequired) && b.skipQeAcceptance === undefined && settingSkip === undefined) {
1831
1851
  return reply.code(400).send({
1832
1852
  error: `workflow ${b.workflow} requires QE acceptance (required_instruments: qe_acceptance), so it delivers only ` +
1833
1853
  'through the deliver gate, where the verdict is checked — deliverGate: "auto" cannot apply to it ' +
@@ -1909,6 +1929,8 @@ runtime = {}) {
1909
1929
  // when sent, never inferred.
1910
1930
  if (b.skipQeAcceptance !== undefined)
1911
1931
  input.skipQeAcceptanceReason = b.skipQeAcceptance.reason;
1932
+ else if (settingSkip !== undefined)
1933
+ input.skipQeAcceptanceReason = settingSkip;
1912
1934
  if (b.forceQeAcceptance === true)
1913
1935
  input.forceQeAcceptance = true;
1914
1936
  // DES-L9 / crew#550 — REVISION: `revisesPr` names an OPEN same-repository pull request whose
@@ -5126,6 +5148,159 @@ runtime = {}) {
5126
5148
  return reply.code(201).send({ terminalId: termId, cli, action });
5127
5149
  });
5128
5150
  }
5151
+ const containerLoginOutcomes = new Map();
5152
+ const containerSeat = (cli) => isContainerSeatFloor() && isContainerLoginCli(cli) && rosterWithStanding().some((seat) => seat.key === cli);
5153
+ const loginError = (stderr) => loginDetail(stderr) || 'seat-login failed';
5154
+ // One sign-in/sign-out at a time per seat, and a generation every new sign-in and every sign-out
5155
+ // bumps (agy crew-r1): an abandoned login terminal that exits later never ingests over a newer
5156
+ // sign-in or a sign-out, and an ingest in flight finishes before a sign-out runs.
5157
+ const loginGeneration = new Map();
5158
+ const seatOps = new Map();
5159
+ const serial = (cli, op) => {
5160
+ const next = (seatOps.get(cli) ?? Promise.resolve()).then(op, op);
5161
+ seatOps.set(cli, next.catch(() => undefined));
5162
+ return next;
5163
+ };
5164
+ const bump = (cli) => {
5165
+ const g = (loginGeneration.get(cli) ?? 0) + 1;
5166
+ loginGeneration.set(cli, g);
5167
+ return g;
5168
+ };
5169
+ // The one sign-in per seat, reserved synchronously from the request's first await until its
5170
+ // ingest settles (codex crew-r1/r2: two sessions would share the engine's login session), and
5171
+ // released only by its owner (the generation that reserved it).
5172
+ const openLogins = new Map();
5173
+ const release = (cli, generation) => {
5174
+ if (openLogins.get(cli) === generation)
5175
+ openLogins.delete(cli);
5176
+ };
5177
+ app.post(`${V}/seats/:cli/container-login`, { config: { manifest: { requestType: 'SeatSessionBody', responseType: 'SeatContainerLoginResponse', statusCodes: [201, 400, 404, 502] } } }, async (req, reply) => {
5178
+ const { cli } = req.params;
5179
+ if (!containerSeat(cli))
5180
+ return reply.code(404).send({ error: `container login unavailable for seat: ${cli}` });
5181
+ const parsed = SeatSessionSchema.safeParse(req.body ?? {});
5182
+ if (!parsed.success)
5183
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
5184
+ if (openLogins.has(cli)) {
5185
+ return reply.code(409).send({ error: `a sign-in is already open for ${cli}; finish or close it first` });
5186
+ }
5187
+ // Reserved BEFORE the first await (codex crew-r1): a sign-out that lands meanwhile wins.
5188
+ const generation = bump(cli);
5189
+ openLogins.set(cli, generation);
5190
+ containerLoginOutcomes.delete(cli);
5191
+ const out = await serial(cli, () => runSeatLogin('argv', cli, seatLoginDeps));
5192
+ if (out.rc !== 0) {
5193
+ release(cli, generation);
5194
+ return reply.code(502).send({ error: loginError(out.stderr) });
5195
+ }
5196
+ if (loginGeneration.get(cli) !== generation) {
5197
+ release(cli, generation);
5198
+ return reply.code(409).send({ error: `the ${cli} sign-in was superseded (a sign-out or another sign-in)` });
5199
+ }
5200
+ let argv;
5201
+ try {
5202
+ const value = JSON.parse(out.stdout);
5203
+ if (!Array.isArray(value) || value.length === 0 || !value.every((arg) => typeof arg === 'string' && arg.length > 0)) {
5204
+ throw new Error('seat-login argv must be a non-empty array of non-empty strings');
5205
+ }
5206
+ argv = value;
5207
+ }
5208
+ catch (err) {
5209
+ release(cli, generation);
5210
+ return reply.code(502).send({ error: message(err) });
5211
+ }
5212
+ let termId = null;
5213
+ let completed = false;
5214
+ const exitedEarly = new Set();
5215
+ const ingest = () => serial(cli, async () => {
5216
+ // A newer sign-in or a sign-out superseded this terminal: nothing to ingest or record.
5217
+ if (loginGeneration.get(cli) !== generation)
5218
+ return;
5219
+ // Refusals the OLD credential earned up to now are superseded by this sign-in (codex
5220
+ // crew-r2: a 401 during the interactive login must not outlive it).
5221
+ const cutoff = Date.now();
5222
+ const result = await runSeatLogin('ingest', cli, seatLoginDeps);
5223
+ // The broker may have changed whatever happens next: the next reading is its own.
5224
+ containerStatus.invalidate(cli);
5225
+ rosterWithStanding.reprobe?.(cli);
5226
+ const outcome = { cli, ok: result.rc === 0, at: new Date().toISOString() };
5227
+ if (result.rc !== 0)
5228
+ outcome.error = loginError(result.stderr);
5229
+ else {
5230
+ try {
5231
+ const value = JSON.parse(result.stdout);
5232
+ if (value === null || !Array.isArray(value.files) || !value.files.every((file) => typeof file === 'string') ||
5233
+ !(value.expires_at === null || (typeof value.expires_at === 'number' && Number.isFinite(value.expires_at)))) {
5234
+ throw new Error('invalid seat-login ingest response');
5235
+ }
5236
+ // Only the public ingest fields cross the wire; raw stdout may contain credentials.
5237
+ outcome.files = value.files;
5238
+ outcome.expires_at = value.expires_at;
5239
+ }
5240
+ catch {
5241
+ outcome.ok = false;
5242
+ outcome.error = 'invalid seat-login ingest response';
5243
+ }
5244
+ }
5245
+ if (outcome.ok) {
5246
+ seatHealth.clearAuthFailureBefore(cli, cutoff);
5247
+ seatHealth.clearRecentBench(cli);
5248
+ }
5249
+ // A sign-out (or a newer sign-in) while this ingest ran owns the outcome now.
5250
+ if (loginGeneration.get(cli) === generation)
5251
+ containerLoginOutcomes.set(cli, outcome);
5252
+ }).finally(() => release(cli, generation));
5253
+ const done = () => {
5254
+ if (completed)
5255
+ return;
5256
+ completed = true;
5257
+ stop();
5258
+ void ingest();
5259
+ };
5260
+ // Subscribe before spawn: an immediate exit still needs exactly one ingest.
5261
+ const stop = adapter.onEvent((event) => {
5262
+ if (event.type !== 'terminalExited' || typeof event.id !== 'string')
5263
+ return;
5264
+ if (termId === null)
5265
+ exitedEarly.add(event.id);
5266
+ else if (event.id === termId)
5267
+ done();
5268
+ });
5269
+ try {
5270
+ termId = await adapter.openTerminal(homedir(), argv, parsed.data.cols ?? 100, parsed.data.rows ?? 30, true);
5271
+ }
5272
+ catch (err) {
5273
+ stop();
5274
+ release(cli, generation);
5275
+ return reply.code(400).send({ error: message(err) });
5276
+ }
5277
+ if (exitedEarly.has(termId))
5278
+ done();
5279
+ return reply.code(201).send({ terminalId: termId, cli, action: 'sign_in' });
5280
+ });
5281
+ app.get(`${V}/seats/:cli/container-login`, { config: { manifest: { responseType: 'SeatContainerLoginOutcome', statusCodes: [200, 404] } } }, async (req, reply) => {
5282
+ const { cli } = req.params;
5283
+ const outcome = containerSeat(cli) ? containerLoginOutcomes.get(cli) : undefined;
5284
+ if (outcome === undefined)
5285
+ return reply.code(404).send({ error: `no container login outcome for seat: ${cli}` });
5286
+ return reply.send(outcome);
5287
+ });
5288
+ app.post(`${V}/seats/:cli/container-logout`, { config: { manifest: { responseType: 'SeatContainerLogoutResponse', statusCodes: [200, 404, 502] } } }, async (req, reply) => {
5289
+ const { cli } = req.params;
5290
+ if (!containerSeat(cli))
5291
+ return reply.code(404).send({ error: `container logout unavailable for seat: ${cli}` });
5292
+ bump(cli);
5293
+ const out = await serial(cli, async () => {
5294
+ containerLoginOutcomes.delete(cli);
5295
+ return runSeatLogin('sign-out', cli, seatLoginDeps);
5296
+ });
5297
+ // Whatever the engine managed before failing, the next reading is the broker's (codex crew-r1).
5298
+ containerStatus.invalidate(cli);
5299
+ rosterWithStanding.reprobe?.(cli);
5300
+ if (out.rc !== 0)
5301
+ return reply.code(502).send({ error: loginError(out.stderr) });
5302
+ return reply.send({ cli, action: 'sign_out' });
5303
+ });
5129
5304
  // Resize a live terminal's PTY.
5130
5305
  app.post(`${V}/terminals/:id/resize`, async (req, reply) => {
5131
5306
  const { id } = req.params;
@@ -5163,6 +5338,9 @@ runtime = {}) {
5163
5338
  // additive `path` (api-types 0.38.0 `SettingsResponse.path?`; adjudicated §4.6 — `path`, not
5164
5339
  // `settings_path`).
5165
5340
  app.get(`${V}/settings`, async () => ({ settings: await adapter.getSettings(), path: settingsFilePath() }));
5341
+ // The demo video's palette from the studio skin (operator 2026-10-14): read-only, derived from `studio.appearance`;
5342
+ // garden's demo stage and interactive's re-cuts read it as their default colours.
5343
+ app.get(`${V}/appearance/video-palette`, { config: { manifest: { responseType: 'VideoPaletteResponse', statusCodes: [200] } } }, async () => videoPalette((await adapter.getSettings())['studio.appearance']));
5166
5344
  app.put(`${V}/settings`, async (req, reply) => {
5167
5345
  const patch = req.body;
5168
5346
  if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
@@ -5278,6 +5456,19 @@ runtime = {}) {
5278
5456
  .send({ error: "deliverDefault must be 'pr' or 'none'" });
5279
5457
  }
5280
5458
  }
5459
+ // qeAcceptance (api-types 0.120.0): 'required' | 'off' only. 'off' skips QE acceptance on every
5460
+ // launch that requires it (labelled with the setting), so a typo must be a 400, never a dropped key.
5461
+ // Turning it off waives an assurance instrument on every launch after it, so only a PERSON may
5462
+ // change it (the predicate the watch switches use); a worker's bearer gets 403.
5463
+ if (Object.hasOwn(patch, 'qeAcceptance')) {
5464
+ const q = patch.qeAcceptance;
5465
+ if (q !== 'required' && q !== 'off') {
5466
+ return reply.code(400).send({ error: "qeAcceptance must be 'required' or 'off'" });
5467
+ }
5468
+ if (!isHumanOperator(actorOf(req))) {
5469
+ return reply.code(403).send({ error: 'Only a person can change QE acceptance (qeAcceptance); a worker or system actor cannot' });
5470
+ }
5471
+ }
5281
5472
  // onboardingAutoCapture (crew#552): whether a completed onboarding chains capture-learnings.
5282
5473
  if (Object.hasOwn(patch, 'onboardingAutoCapture') && typeof patch.onboardingAutoCapture !== 'boolean') {
5283
5474
  return reply.code(400).send({ error: 'onboardingAutoCapture must be true or false' });
@@ -5362,6 +5553,7 @@ runtime = {}) {
5362
5553
  'workerStallEscalateAction',
5363
5554
  'workerStallMaxEscalations',
5364
5555
  'deliverDefault',
5556
+ 'qeAcceptance',
5365
5557
  'onboardingAutoCapture',
5366
5558
  'deliverIdentityLogin',
5367
5559
  'deliverIdentityByRepo',
@@ -6035,6 +6227,35 @@ runtime = {}) {
6035
6227
  dropDocLedgerRows: runtime.dropDocLedgerRows ?? (() => ({ ok: true, removed_keys: [] })),
6036
6228
  log: (m) => app.log.warn(m),
6037
6229
  });
6230
+ // ── A demo run's video as an editable document (the video editor re-cuts it through E1's per-doc API) ──
6231
+ registerDemoDocumentRoute(app, adapter, {
6232
+ bridgeFor: async (projectId) => {
6233
+ const root = await projectDocsRoot(adapter, projectSettings, projectId);
6234
+ if (root === null)
6235
+ return null;
6236
+ const bridge = await interactiveBridges.ensure(root);
6237
+ return async (method, path, body) => {
6238
+ const raw = Buffer.isBuffer(body);
6239
+ const res = await fetch(`http://${bridge.host}:${bridge.port}${path}`, {
6240
+ method,
6241
+ headers: { 'content-type': raw ? 'application/octet-stream' : 'application/json' },
6242
+ body: raw ? new Uint8Array(body) : JSON.stringify(body),
6243
+ // A segment upload or the import's render (ffmpeg) can take minutes on a loaded host.
6244
+ signal: AbortSignal.timeout(600_000),
6245
+ });
6246
+ let parsed = {};
6247
+ try {
6248
+ const j = await res.json();
6249
+ if (j !== null && typeof j === 'object' && !Array.isArray(j))
6250
+ parsed = j;
6251
+ }
6252
+ catch {
6253
+ parsed = {};
6254
+ }
6255
+ return { status: res.status, body: parsed };
6256
+ };
6257
+ },
6258
+ });
6038
6259
  // ── Project-attributed docs list (crew#472) ──
6039
6260
  // `GET /projects/:id/interactive/api/docs` relays the bridge's list and stamps each row with
6040
6261
  // the mount's `projectId` — the attribution the bridge cannot know. Same static-over-wildcard