wicked-crew 0.8.7 → 0.8.9
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/dist/api/chat-transcripts.d.ts +3 -19
- package/dist/api/chat-transcripts.d.ts.map +1 -1
- package/dist/api/chat-transcripts.js +8 -32
- package/dist/api/chat-transcripts.js.map +1 -1
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +8 -3
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/roster-standing.d.ts +2 -0
- package/dist/api/roster-standing.d.ts.map +1 -1
- package/dist/api/roster-standing.js +3 -0
- package/dist/api/roster-standing.js.map +1 -1
- package/dist/api/routes.d.ts +45 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +165 -12
- package/dist/api/routes.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +4 -0
- package/dist/api/server.js.map +1 -1
- package/dist/api/unit-output.d.ts +17 -0
- package/dist/api/unit-output.d.ts.map +1 -1
- package/dist/api/unit-output.js +47 -0
- package/dist/api/unit-output.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +3 -2
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +15 -11
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-path.d.ts +24 -3
- package/dist/core/bridge-path.d.ts.map +1 -1
- package/dist/core/bridge-path.js +96 -6
- package/dist/core/bridge-path.js.map +1 -1
- package/dist/core/deliver-text.d.ts +10 -1
- package/dist/core/deliver-text.d.ts.map +1 -1
- package/dist/core/deliver-text.js +63 -2
- package/dist/core/deliver-text.js.map +1 -1
- package/dist/core/deliver-triage.d.ts +21 -1
- package/dist/core/deliver-triage.d.ts.map +1 -1
- package/dist/core/deliver-triage.js +72 -2
- package/dist/core/deliver-triage.js.map +1 -1
- package/dist/core/deliver.d.ts +19 -0
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +55 -8
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/host-paths.d.ts +33 -0
- package/dist/core/host-paths.d.ts.map +1 -0
- package/dist/core/host-paths.js +95 -0
- package/dist/core/host-paths.js.map +1 -0
- package/dist/core/linked-issues.d.ts +20 -2
- package/dist/core/linked-issues.d.ts.map +1 -1
- package/dist/core/linked-issues.js +41 -6
- package/dist/core/linked-issues.js.map +1 -1
- package/dist/core/worker-env.d.ts +31 -0
- package/dist/core/worker-env.d.ts.map +1 -0
- package/dist/core/worker-env.js +34 -0
- package/dist/core/worker-env.js.map +1 -0
- package/dist/interactive/bridge-pool.d.ts +27 -2
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +40 -3
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/projects/routes.d.ts +2 -2
- package/dist/skills/runtime.d.ts +56 -1
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +135 -2
- package/dist/skills/runtime.js.map +1 -1
- package/dist/skills/store.d.ts +27 -0
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +63 -0
- package/dist/skills/store.js.map +1 -1
- package/dist/studio/assets/index-DofYURLG.js +564 -0
- package/dist/studio/assets/index-fSSul5Hu.css +32 -0
- package/dist/studio/editors/wicked-page/editor.json +1 -1
- package/dist/studio/editors/wicked-page/index.html +1 -1
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +942 -4529
- package/endpoint-manifest.json +33 -1
- package/package.json +3 -3
- package/dist/studio/assets/index-CUNMURZ4.js +0 -581
- package/dist/studio/assets/index-DPqNRzrT.css +0 -32
package/dist/api/routes.js
CHANGED
|
@@ -3,6 +3,7 @@ import { PlanSchema, toLaunchPlan } from './plan-schema.js';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { listRequirements, getRequirement, patchRequirement } from './requirements.js';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
6
7
|
import { readFileSync, existsSync } from 'node:fs';
|
|
7
8
|
import { promises as fsp } from 'node:fs';
|
|
8
9
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
@@ -14,7 +15,7 @@ import { detectRefusal } from './gate-cache.js';
|
|
|
14
15
|
import { acceptanceRequirementOf, buildAcceptanceView } from '../qe/acceptance.js';
|
|
15
16
|
import { runIdentityOf, wireIdentity } from '../core/run-identity.js';
|
|
16
17
|
import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
|
|
17
|
-
import { outputUnavailableReason, resolveUnit, unitKeysFor } from './unit-output.js';
|
|
18
|
+
import { attemptUnavailableReason, capturedAttempts, outputUnavailableReason, resolveUnit, storedAttempt, unitKeysFor } from './unit-output.js';
|
|
18
19
|
import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
|
|
19
20
|
import { callEstateTool, EstateMcpError, memoryStoreInfo } from '../core/estate-mcp-client.js';
|
|
20
21
|
import { SeatHealthTracker } from './seat-health.js';
|
|
@@ -148,15 +149,28 @@ function message(err) {
|
|
|
148
149
|
* the caller comparing their JSON against the source, so the unknown keys are named in `error`
|
|
149
150
|
* itself, where a human and a `curl | jq .error` both see it without reading `details`.
|
|
150
151
|
*/
|
|
151
|
-
function invalidBody(err, what) {
|
|
152
|
+
export function invalidBody(err, what) {
|
|
152
153
|
const unknown = err.issues.flatMap((i) => (i.code === 'unrecognized_keys' ? i.keys : []));
|
|
153
154
|
const error = unknown.length > 0
|
|
154
155
|
? `${what}: unknown field${unknown.length > 1 ? 's' : ''} ${unknown
|
|
155
156
|
.map((k) => `\`${k}\``)
|
|
156
157
|
.join(', ')} — this endpoint does not accept ${unknown.length > 1 ? 'them' : 'it'}, and ignoring ${unknown.length > 1 ? 'them' : 'it'} would run a different request than you sent`
|
|
157
|
-
: what;
|
|
158
|
+
: namedIssues(what, err.issues);
|
|
158
159
|
return { error, details: err.issues };
|
|
159
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* crew#662: a schema rejection that is NOT an unknown key names the field and what was expected —
|
|
163
|
+
* `Invalid request body: \`revisesPr\` — Expected number, received boolean`. The bare
|
|
164
|
+
* "Invalid request body" sent a caller off to misdiagnose the contract. Up to three issues are
|
|
165
|
+
* spelled out; the rest are counted (all of them stay in `details`).
|
|
166
|
+
*/
|
|
167
|
+
function namedIssues(what, issues) {
|
|
168
|
+
if (issues.length === 0)
|
|
169
|
+
return what;
|
|
170
|
+
const shown = issues.slice(0, 3).map((i) => (i.path.length > 0 ? `\`${i.path.join('.')}\` — ${i.message}` : i.message));
|
|
171
|
+
const more = issues.length > 3 ? `; and ${issues.length - 3} more (see details)` : '';
|
|
172
|
+
return `${what}: ${shown.join('; ')}${more}`;
|
|
173
|
+
}
|
|
160
174
|
/** First value of a possibly-repeated query param, WITHOUT trimming — `''` is PRESERVED because a
|
|
161
175
|
* blank `scope`/`scope_prefix` is meaningful to estate (root / whole-subtree), unlike an omitted
|
|
162
176
|
* one. Returns undefined only when the param is absent. */
|
|
@@ -313,6 +327,11 @@ const OnboardBodySchema = z
|
|
|
313
327
|
// compile time, that every body the published contract (`wicked-crew-api-types`) lets a client
|
|
314
328
|
// send is a body these schemas accept — the request-direction half of the drift guard (task #84).
|
|
315
329
|
export { PlanSchema };
|
|
330
|
+
/** crew#825: the issue refs a launch leaves out of the linked-issue expansion. */
|
|
331
|
+
const ExcludeLinkedIssuesSchema = z
|
|
332
|
+
.array(z.string().regex(/^\s*(?:\d+|(?:(?:[\w.-]+\/)?[\w.-]+)?#\d+)\s*$/, 'an issue ref: 541, #541, repo#541 or owner/repo#541'))
|
|
333
|
+
.max(50)
|
|
334
|
+
.optional();
|
|
316
335
|
export const LaunchSchema = z.object({
|
|
317
336
|
problem: z.string().min(1),
|
|
318
337
|
sessionId: z.string().min(1).optional(),
|
|
@@ -375,6 +394,11 @@ export const LaunchSchema = z.object({
|
|
|
375
394
|
* launch (e.g. a CI job name or a studio tab id). Persisted on the `run.launched` audit entry
|
|
376
395
|
* (`detail.actor`) and served on the run DTO; absent = caller omitted it. */
|
|
377
396
|
actor: z.string().min(1).max(256).optional(),
|
|
397
|
+
/** crew#825 (api-types additive) — issue refs the intent names that the daemon must NOT read and
|
|
398
|
+
* append (`541`, `#541`, `repo#541`, `owner/repo#541`): a reference in passing is not a request to inline
|
|
399
|
+
* the issue. They are answered as `linkedIssues[].excluded`. Send only when
|
|
400
|
+
* `GET /health.capabilities.linkedIssuesExclude === true`. */
|
|
401
|
+
excludeLinkedIssues: ExcludeLinkedIssuesSchema,
|
|
378
402
|
}).strict().refine((b) => b.plan === undefined || b.workflow === undefined, {
|
|
379
403
|
message: 'plan and workflow are mutually exclusive — a launch carries a plan or names a preset, not both',
|
|
380
404
|
path: ['plan'],
|
|
@@ -461,6 +485,11 @@ export const OpenTerminalSchema = z.object({
|
|
|
461
485
|
// default; the ungoverned operator shell must opt in explicitly).
|
|
462
486
|
governed: z.boolean().optional(),
|
|
463
487
|
}).strict();
|
|
488
|
+
/** `POST /seats/:cli/login|logout` body (crew#615): the PTY size, both optional. */
|
|
489
|
+
export const SeatSessionSchema = z.object({
|
|
490
|
+
cols: z.number().int().positive().optional(),
|
|
491
|
+
rows: z.number().int().positive().optional(),
|
|
492
|
+
}).strict();
|
|
464
493
|
const ResizeTerminalSchema = z.object({
|
|
465
494
|
cols: z.number().int().positive(),
|
|
466
495
|
rows: z.number().int().positive(),
|
|
@@ -764,7 +793,8 @@ runtime = {}) {
|
|
|
764
793
|
/** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
|
|
765
794
|
const resolvePullRequest = runtime.resolvePullRequest ?? resolvePullRequestViaGh;
|
|
766
795
|
/** crew#627: the intent's linked issues, read under the daemon's identity at launch. */
|
|
767
|
-
const readLinkedIssues = runtime.resolveLinkedIssues ??
|
|
796
|
+
const readLinkedIssues = runtime.resolveLinkedIssues ??
|
|
797
|
+
((p, root, ref, exclude) => resolveLinkedIssues(p, root, ref, undefined, exclude));
|
|
768
798
|
/** crew#550 P-7: the follow-ups the run's evaluator units flagged, read from their captured output.
|
|
769
799
|
* A unit whose output cannot be read contributes nothing (the section then says what WAS read);
|
|
770
800
|
* an adapter without transcripts (a directly-driven route set) reads as "not read" — absent. */
|
|
@@ -1000,6 +1030,8 @@ runtime = {}) {
|
|
|
1000
1030
|
: { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false }),
|
|
1001
1031
|
// C1: `AgentSession.chat_id` is served from the daemon's own launch index, whatever the engine.
|
|
1002
1032
|
runChatId: true,
|
|
1033
|
+
// crew#825: `LaunchRunBody.excludeLinkedIssues` + `POST /linked-issues/preview` are served.
|
|
1034
|
+
linkedIssuesExclude: true,
|
|
1003
1035
|
// ASK-C1 (DES-ASK-TEAM-CHAT-001 §5.1): an ask starts a path — the whole contract is the
|
|
1004
1036
|
// pinned core-ts floor the adapter reports; an older studio reads ABSENT as "every helper
|
|
1005
1037
|
// answers", a newer one renders the one PA voice.
|
|
@@ -1586,6 +1618,33 @@ runtime = {}) {
|
|
|
1586
1618
|
const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, body.data);
|
|
1587
1619
|
return reply.code(201).send({ runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
|
|
1588
1620
|
});
|
|
1621
|
+
// crew#825: what a workflow launch WOULD append — the same reading `POST /runs` does, before Send,
|
|
1622
|
+
// so the composer shows "N linked issues will be appended (≈K chars)" with a way to leave one out
|
|
1623
|
+
// (`excludeLinkedIssues`). Read-only: nothing launches, nothing is stored.
|
|
1624
|
+
const LinkedIssuesPreviewSchema = z
|
|
1625
|
+
.object({
|
|
1626
|
+
problem: z.string().min(1),
|
|
1627
|
+
repoRef: z.string().min(1).optional(),
|
|
1628
|
+
excludeLinkedIssues: ExcludeLinkedIssuesSchema,
|
|
1629
|
+
})
|
|
1630
|
+
.strict();
|
|
1631
|
+
app.post(`${V}/linked-issues/preview`, { config: { manifest: { requestType: 'LinkedIssuesPreviewBody', responseType: 'LinkedIssuesPreviewResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
|
|
1632
|
+
const parsed = LinkedIssuesPreviewSchema.safeParse(req.body);
|
|
1633
|
+
if (!parsed.success)
|
|
1634
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1635
|
+
const b = parsed.data;
|
|
1636
|
+
// The launch's own best-effort contract (codex on this PR): a resolver fault launches on the
|
|
1637
|
+
// intent alone, so the preview answers "nothing appended" and says why — never a 500.
|
|
1638
|
+
try {
|
|
1639
|
+
const root = b.repoRef !== undefined ? await repoRootOf(b.repoRef) : undefined;
|
|
1640
|
+
const linked = await readLinkedIssues(b.problem, root, b.repoRef, b.excludeLinkedIssues ?? []);
|
|
1641
|
+
return { issues: linked.issues, appendedChars: linked.block === null ? 0 : linked.block.length + 2 };
|
|
1642
|
+
}
|
|
1643
|
+
catch (err) {
|
|
1644
|
+
req.log.warn(`linked-issue preview failed; a launch would run on the intent alone: ${message(err)}`);
|
|
1645
|
+
return { issues: [], appendedChars: 0, error: `linked issues could not be read: ${message(err)} — a launch now runs on the intent alone` };
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1589
1648
|
// Launch a run (replaces POST /sessions). `clisJson` defaults to the roster;
|
|
1590
1649
|
// `sessionId` is minted if the client omits it.
|
|
1591
1650
|
app.post(`${V}/runs`, {
|
|
@@ -1798,11 +1857,12 @@ runtime = {}) {
|
|
|
1798
1857
|
// fault (the repo list unreadable, say) launches the run on its intent alone and is logged.
|
|
1799
1858
|
try {
|
|
1800
1859
|
const root = b.repoRef !== undefined ? await repoRootOf(b.repoRef) : undefined;
|
|
1801
|
-
const linked = await readLinkedIssues(b.problem, root, b.repoRef);
|
|
1802
|
-
if (linked.block !== null)
|
|
1860
|
+
const linked = await readLinkedIssues(b.problem, root, b.repoRef, b.excludeLinkedIssues ?? []);
|
|
1861
|
+
if (linked.block !== null)
|
|
1803
1862
|
input.problem = `${b.problem.trimEnd()}\n\n${linked.block}`;
|
|
1863
|
+
// crew#825: an all-excluded intent appends nothing but still answers what it left out.
|
|
1864
|
+
if (linked.issues.length > 0)
|
|
1804
1865
|
linkedIssues = linked.issues;
|
|
1805
|
-
}
|
|
1806
1866
|
}
|
|
1807
1867
|
catch (err) {
|
|
1808
1868
|
req.log.warn(`linked-issue resolution failed; launching on the intent alone: ${message(err)}`);
|
|
@@ -1899,7 +1959,11 @@ runtime = {}) {
|
|
|
1899
1959
|
// planner (one unit, the brief verbatim). Say so in the answer, with how to get work done,
|
|
1900
1960
|
// instead of letting it pass for a launch of real work.
|
|
1901
1961
|
const freeText = input.workflow === undefined && input.plan === undefined ? { freeText: { notice: FREE_TEXT_NOTICE } } : {};
|
|
1902
|
-
|
|
1962
|
+
// crew#874: the run uses the PUBLISHED generation; when the installed garden is not the one it
|
|
1963
|
+
// was built from, the launch answer says so (a recorded proof can disclose which garden ran).
|
|
1964
|
+
const skillsAhead = runtime.skills?.installedAheadFinding({ fresh: true }) ?? null; // fresh: an upgrade just before Send counts (codex r1)
|
|
1965
|
+
const skillsWarning = skillsAhead === null ? {} : { skillsWarning: { kind: skillsAhead.kind, severity: skillsAhead.severity, message: skillsAhead.message } };
|
|
1966
|
+
return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText, ...skillsWarning });
|
|
1903
1967
|
}
|
|
1904
1968
|
catch (err) {
|
|
1905
1969
|
const msg = message(err);
|
|
@@ -3009,7 +3073,9 @@ runtime = {}) {
|
|
|
3009
3073
|
refused: chatScopes.refusedOf(id) ?? null,
|
|
3010
3074
|
...(askPaths.view(id) !== undefined ? { path: askPaths.view(id) } : {}),
|
|
3011
3075
|
// DES-L5 (D-13): the transcript so far, append order — `[]` before the first persisted
|
|
3012
|
-
// turn
|
|
3076
|
+
// turn. The file goes with the chat on `chatClosed` UNLESS a run promoted from it is still
|
|
3077
|
+
// live (crew#619 retention — dropped when that run terminals), so a reclaimed id answers
|
|
3078
|
+
// `[]` only once no promoted run holds the old file.
|
|
3013
3079
|
...(chatTranscripts !== undefined ? { messages: chatTranscripts.read(id) } : {}),
|
|
3014
3080
|
};
|
|
3015
3081
|
}
|
|
@@ -3085,12 +3151,48 @@ runtime = {}) {
|
|
|
3085
3151
|
units: unitKeysFor(run),
|
|
3086
3152
|
});
|
|
3087
3153
|
}
|
|
3154
|
+
const rawAttempt = req.query.attempt;
|
|
3155
|
+
const attemptArg = Array.isArray(rawAttempt) ? rawAttempt[0] : rawAttempt;
|
|
3156
|
+
const wanted = attemptArg === undefined || attemptArg === '' ? undefined : /^\d{1,9}$/.test(attemptArg) ? Number(attemptArg) : NaN;
|
|
3157
|
+
if (wanted !== undefined && !Number.isSafeInteger(wanted)) {
|
|
3158
|
+
return reply.code(400).send({ error: '`attempt` must be a non-negative integer' });
|
|
3159
|
+
}
|
|
3088
3160
|
// Keyed off the unit RECORD (the same derivation the evidence bundle uses), not off the
|
|
3089
3161
|
// caller's path segment — the two disagreeing is what made a resolvable unit unreadable.
|
|
3090
3162
|
const output = await adapter.workOutput(coreUnitId(id, unit));
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3163
|
+
// crew#848: which attempt those bytes are. The engine keeps one transcript per unit (the last
|
|
3164
|
+
// attempt that folded ok); the durable log has one `unitOutputCaptured` per attempt.
|
|
3165
|
+
const events = typeof adapter.runEvents === 'function' ? await adapter.runEvents(id).catch(() => null) : null;
|
|
3166
|
+
const attempts = capturedAttempts((events ?? []), unit.ord);
|
|
3167
|
+
const served = output !== null ? storedAttempt(attempts, output) : null;
|
|
3168
|
+
const latest = attempts[attempts.length - 1];
|
|
3169
|
+
const meta = {
|
|
3170
|
+
...(served !== null ? { attempt: served } : {}),
|
|
3171
|
+
...(latest !== undefined ? { latestAttempt: latest.attempt } : {}),
|
|
3172
|
+
...(events !== null ? { attempts } : {}),
|
|
3173
|
+
};
|
|
3174
|
+
if (served !== null)
|
|
3175
|
+
void reply.header('x-wicked-unit-attempt', String(served));
|
|
3176
|
+
if (wanted !== undefined) {
|
|
3177
|
+
const rec = attempts.find((a) => a.attempt === wanted);
|
|
3178
|
+
if (rec === undefined) {
|
|
3179
|
+
return reply.code(404).send({
|
|
3180
|
+
error: `Unit ${unit.ord} of run ${id} has no attempt ${wanted} on record`,
|
|
3181
|
+
attempts: attempts.map((a) => a.attempt),
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
if (output !== null && served === wanted)
|
|
3185
|
+
return reply.send({ output, ...meta });
|
|
3186
|
+
return reply.send({ output: null, ...meta, attempt: wanted, outputUnavailable: attemptUnavailableReason(unit, rec, served) });
|
|
3187
|
+
}
|
|
3188
|
+
if (output !== null) {
|
|
3189
|
+
// A later attempt that left no transcript: the stored bytes are SUPERSEDED — say so, and why.
|
|
3190
|
+
if (served !== null && latest !== undefined && latest.attempt > served) {
|
|
3191
|
+
return reply.send({ output, ...meta, superseded: true, latestUnavailable: attemptUnavailableReason(unit, latest, served) });
|
|
3192
|
+
}
|
|
3193
|
+
return reply.send({ output, ...meta });
|
|
3194
|
+
}
|
|
3195
|
+
return reply.send({ output: null, ...meta, outputUnavailable: outputUnavailableReason(unit) });
|
|
3094
3196
|
});
|
|
3095
3197
|
// DC-S7 (§4.6): what the seats considered, set aside and cited for one chat turn or one unit
|
|
3096
3198
|
// attempt. Operator+ (the rules are the project's doctrine). A cited rule is "unchecked", never
|
|
@@ -4617,6 +4719,57 @@ runtime = {}) {
|
|
|
4617
4719
|
return reply.code(400).send({ error: message(err) });
|
|
4618
4720
|
}
|
|
4619
4721
|
});
|
|
4722
|
+
// crew#615: sign a seat in or out without the client composing the command. The daemon runs the
|
|
4723
|
+
// ENGINE's own `login_invocation` / `logout_invocation` for that seat (already prefixed with the
|
|
4724
|
+
// seat's configuration root; the daemon never synthesizes one) in a governed PTY, and answers
|
|
4725
|
+
// the terminal id — the device URL or the CLI's prompt streams over `/ws/terminals/:id`. The
|
|
4726
|
+
// seat's credential probe is re-run when the terminal exits, so the roster's `signed_in` follows
|
|
4727
|
+
// the outcome instead of waiting out the probe's TTL.
|
|
4728
|
+
for (const action of ['login', 'logout']) {
|
|
4729
|
+
app.post(`${V}/seats/:cli/${action}`, { config: { manifest: { requestType: 'SeatSessionBody', responseType: 'SeatSessionResponse', statusCodes: [201, 400, 404] } } }, async (req, reply) => {
|
|
4730
|
+
const { cli } = req.params;
|
|
4731
|
+
const parsed = SeatSessionSchema.safeParse(req.body ?? {});
|
|
4732
|
+
if (!parsed.success) {
|
|
4733
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
4734
|
+
}
|
|
4735
|
+
const seat = rosterWithStanding().find((s) => s.key === cli);
|
|
4736
|
+
if (seat === undefined)
|
|
4737
|
+
return reply.code(404).send({ error: `unknown seat: ${cli}` });
|
|
4738
|
+
const field = action === 'login' ? seat.login_invocation : seat.logout_invocation;
|
|
4739
|
+
const invocation = typeof field === 'string' ? field.trim() : '';
|
|
4740
|
+
if (invocation === '') {
|
|
4741
|
+
return reply.code(404).send({
|
|
4742
|
+
error: `seat ${cli} has no ${action} command: the engine roster carries no ${action}_invocation for it (its CLI documents none)`,
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
// Subscribe BEFORE the spawn: a short command can exit before `openTerminal` resolves, and
|
|
4746
|
+
// its exit must still re-probe and release the listener (codex review).
|
|
4747
|
+
let termId = null;
|
|
4748
|
+
const exitedEarly = new Set();
|
|
4749
|
+
const done = () => {
|
|
4750
|
+
stop();
|
|
4751
|
+
rosterWithStanding.reprobe?.(cli);
|
|
4752
|
+
};
|
|
4753
|
+
const stop = adapter.onEvent((event) => {
|
|
4754
|
+
if (event.type !== 'terminalExited' || typeof event.id !== 'string')
|
|
4755
|
+
return;
|
|
4756
|
+
if (termId === null)
|
|
4757
|
+
exitedEarly.add(event.id);
|
|
4758
|
+
else if (event.id === termId)
|
|
4759
|
+
done();
|
|
4760
|
+
});
|
|
4761
|
+
try {
|
|
4762
|
+
termId = await adapter.openTerminal(homedir(), ['sh', '-lc', invocation], parsed.data.cols ?? 100, parsed.data.rows ?? 30, true);
|
|
4763
|
+
}
|
|
4764
|
+
catch (err) {
|
|
4765
|
+
stop();
|
|
4766
|
+
return reply.code(400).send({ error: message(err) });
|
|
4767
|
+
}
|
|
4768
|
+
if (exitedEarly.has(termId))
|
|
4769
|
+
done();
|
|
4770
|
+
return reply.code(201).send({ terminalId: termId, cli, action });
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4620
4773
|
// Resize a live terminal's PTY.
|
|
4621
4774
|
app.post(`${V}/terminals/:id/resize`, async (req, reply) => {
|
|
4622
4775
|
const { id } = req.params;
|