@sigrun/sigrun 0.6.0 → 0.7.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 +102 -0
- package/README.md +105 -18
- package/dist/blocks/build-stamp.json +2 -2
- package/dist/blocks/schema.js +5 -0
- package/dist/cli.js +85 -22
- package/dist/curation/host.js +46 -21
- package/dist/curation/migrate.js +96 -0
- package/dist/curation/overlays.js +52 -16
- package/dist/curation/paths.js +22 -0
- package/dist/curation/runs.js +34 -17
- package/dist/curation/skill-tool.js +34 -9
- package/dist/curation/store.js +382 -254
- package/dist/curation/targets.js +21 -0
- package/dist/curation/wrap.js +1 -6
- package/dist/extension.d.ts +12 -3
- package/dist/harness/boot.js +143 -6
- package/dist/harness/event-bus.js +19 -0
- package/dist/harness/loader.js +22 -0
- package/dist/harness/packages.js +7 -0
- package/dist/harness/reserved-sections.js +2 -0
- package/dist/harness/seam-host.js +71 -16
- package/dist/harness/workspace.js +34 -1
- package/dist/history/git.js +707 -0
- package/dist/history/trailers.js +145 -0
- package/dist/history/workspace-history.js +47 -0
- package/dist/jobs/index.js +5 -3
- package/dist/jobs/reconcile.js +2 -3
- package/dist/jobs/state-dir.js +9 -0
- package/dist/jobs/transfer.js +6 -6
- package/dist/sandbox/grants.js +11 -0
- package/dist/sandbox/launch.js +19 -9
- package/dist/sandbox/policy.js +6 -1
- package/dist/serve.js +3 -0
- package/dist/server/auth/proxy-only.js +5 -4
- package/dist/server/core-views/index.js +26 -0
- package/dist/server/core-views/jobs.js +661 -0
- package/dist/server/core-views/skills-tree.js +628 -0
- package/dist/server/core-views/skills.js +1090 -0
- package/dist/server/index.js +88 -0
- package/dist/server/jobs-access.js +62 -0
- package/dist/server/notifications.js +24 -12
- package/dist/server/routes/extensions.js +9 -1
- package/dist/server/routes/jobs.js +19 -52
- package/dist/server/routes/meta.js +9 -0
- package/dist/server/routes/onboarding.js +13 -1
- package/dist/server/surfaces/service.js +38 -0
- package/dist/skills/index.js +1 -1
- package/dist/skills/roots.js +301 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +13 -0
- package/node_modules/@sigrun/shared/dist/auth.d.ts +22 -0
- package/node_modules/@sigrun/shared/dist/auth.js +26 -0
- package/node_modules/@sigrun/shared/dist/blocks/index.d.ts +1 -1
- package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +15 -0
- package/node_modules/@sigrun/shared/dist/model-label.d.ts +29 -0
- package/node_modules/@sigrun/shared/dist/model-label.js +55 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +28 -0
- package/node_modules/@sigrun/shared/package.json +9 -1
- package/package.json +7 -7
- package/webui/dist/assets/index-BI5k-Oci.js +55 -0
- package/webui/dist/assets/index-EGluyRXk.css +1 -0
- package/webui/dist/favicon.svg +7 -5
- package/webui/dist/index.html +3 -3
- package/webui/dist/assets/index-BR0di0OG.css +0 -1
- package/webui/dist/assets/index-BzrA1T4g.js +0 -55
package/dist/server/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import Fastify from "fastify";
|
|
|
34
34
|
import { join } from "node:path";
|
|
35
35
|
import { LoginFlows } from "./login/flows.js";
|
|
36
36
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
37
|
+
import { SessionMetaDoc } from "./session-meta.js";
|
|
37
38
|
import { buildCommandCatalog, SLASH_COMMANDS } from "./commands.js";
|
|
38
39
|
import { effectiveMethods } from "../config.js";
|
|
39
40
|
import { registerAuthPipeline } from "./auth/pipeline.js";
|
|
@@ -41,6 +42,12 @@ import { isLoopbackHost } from "./auth/loopback.js";
|
|
|
41
42
|
import { SseRegistry } from "./sse-registry.js";
|
|
42
43
|
import { storeAgentChange } from "./helpers.js";
|
|
43
44
|
import { registerCoreOwnershipParticipant } from "./ownership.js";
|
|
45
|
+
import { createJobsView } from "./core-views/jobs.js";
|
|
46
|
+
import { createSkillsView } from "./core-views/skills.js";
|
|
47
|
+
import { learnedSkillsDir } from "../curation/paths.js";
|
|
48
|
+
import { primaryAdmin } from "../harness/identity/primary-admin.js";
|
|
49
|
+
import { isSuperuser } from "./auth/principal.js";
|
|
50
|
+
import { accessPrincipalOf } from "./auth/access-principal.js";
|
|
44
51
|
import { createHostBridge } from "./host-bridge.js";
|
|
45
52
|
import { registerJobsOwnershipParticipant } from "../jobs/transfer.js";
|
|
46
53
|
import { registerAdminRoutes } from "./routes/admin.js";
|
|
@@ -108,6 +115,13 @@ import { resolveWebuiBundle, serveWebuiBundle, } from "./static.js";
|
|
|
108
115
|
* env-less boot).
|
|
109
116
|
*/
|
|
110
117
|
export async function createServer(opened, config, contributedBundles = [], options) {
|
|
118
|
+
// Polish A1 (spec §5.2): a server EXISTS here, so every seam host's
|
|
119
|
+
// bridge is expected — synchronously, before the first await, so a
|
|
120
|
+
// pre-bind call made while this function opens keeps the bounded wait
|
|
121
|
+
// instead of failing fast. Rigs that start a server need no open
|
|
122
|
+
// option; `serve`/`webui/e2e` additionally pass `expectHostBridge:
|
|
123
|
+
// true` at open for the window BEFORE this call.
|
|
124
|
+
opened.expectHostBridge();
|
|
111
125
|
const app = Fastify({
|
|
112
126
|
logger: false,
|
|
113
127
|
// U7 Task 3: the image route's GET `src=data:` form carries up to
|
|
@@ -273,6 +287,44 @@ export async function createServer(opened, config, contributedBundles = [], opti
|
|
|
273
287
|
})),
|
|
274
288
|
},
|
|
275
289
|
]);
|
|
290
|
+
// The core-owned views (tabs+skills Tasks 4/5/12): the reserved `core`
|
|
291
|
+
// slug's `SurfaceDef<"view">` list — the built-in Jobs view (Task 5)
|
|
292
|
+
// and the built-in Skills view (Task 12). The jobs view shares the
|
|
293
|
+
// routes' access rule (`jobs-access.ts`) and the routes' principal
|
|
294
|
+
// projection (accessPrincipalOf over the live directory), so the
|
|
295
|
+
// view and `GET /v1/jobs` always agree on which rows a caller sees.
|
|
296
|
+
// The skills view resolves every root per CALLER (their workspace +
|
|
297
|
+
// learned repos, the deployment's config dir and package roots) and
|
|
298
|
+
// bumps every rendered instance after every write.
|
|
299
|
+
surfacesService.attachCoreViews([
|
|
300
|
+
createJobsView({
|
|
301
|
+
jobs: opened.jobs,
|
|
302
|
+
now: () => new Date(),
|
|
303
|
+
accessPrincipal: (principal) => accessPrincipalOf(principal, opened.identity.users),
|
|
304
|
+
}),
|
|
305
|
+
createSkillsView({
|
|
306
|
+
curationOf: (user) => opened.curationOf(user),
|
|
307
|
+
workspaceRoot: (user) => opened.workspaceRootOf(user),
|
|
308
|
+
workspaceHistoryFor: (user) => opened.history.workspace(user),
|
|
309
|
+
learnedDirOf: (user) => {
|
|
310
|
+
if (opened.identity.users.get(user) === undefined)
|
|
311
|
+
return undefined;
|
|
312
|
+
return learnedSkillsDir(join(opened.extensionStores.roots.dataDir, "users", user));
|
|
313
|
+
},
|
|
314
|
+
userDir: join(opened.extensionStores.roots.configDir, "skills"),
|
|
315
|
+
packageRoots: () => opened.skillRoots,
|
|
316
|
+
bump: () => surfacesService.bumpAllInstances("core", "skills"),
|
|
317
|
+
// Whole-branch review I3: the userless superuser (the naive
|
|
318
|
+
// single-operator principal) acts as the PRIMARY ADMIN — the
|
|
319
|
+
// identity package's own seam (the jail's fallback). Absent (an
|
|
320
|
+
// empty directory), the superuser keeps its own scope. An
|
|
321
|
+
// anonymous NON-superuser never resolves (access none).
|
|
322
|
+
resolveUserlessCaller: (principal) => principal.user ??
|
|
323
|
+
(isSuperuser(principal)
|
|
324
|
+
? primaryAdmin(opened.identity.users.list())?.user
|
|
325
|
+
: undefined),
|
|
326
|
+
}),
|
|
327
|
+
]);
|
|
276
328
|
app.decorate("surfaces", surfacesService);
|
|
277
329
|
// The chat-block change seam (UI-blocks U5 Task 2, spec §8.2):
|
|
278
330
|
// show_blocks/update_blocks AND the user-edit route publish
|
|
@@ -342,6 +394,42 @@ export async function createServer(opened, config, contributedBundles = [], opti
|
|
|
342
394
|
// the first event) and never stopped — the surface rides the server
|
|
343
395
|
// lifetime, like the routes above.
|
|
344
396
|
startNotifications(ctx);
|
|
397
|
+
// The jobs view's freshness (tabs+skills Task 5): every `job.changed`
|
|
398
|
+
// host event bumps every rendered `core/jobs` instance's rev through
|
|
399
|
+
// the bus's bounded side lane (the hub contract — no work inline);
|
|
400
|
+
// watching clients refetch the data document (the tree is static).
|
|
401
|
+
opened.events.subscribe((event) => {
|
|
402
|
+
if (event.type !== "job.changed")
|
|
403
|
+
return;
|
|
404
|
+
void opened.eventBus.runBounded("core/jobs job.changed", () => {
|
|
405
|
+
surfacesService.bumpAllInstances("core", "jobs");
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
// Workspace skills history (tabs+skills Task 10): every settled run
|
|
409
|
+
// catches up the conversation owner's `<workspace>/skills` repo on the
|
|
410
|
+
// bus's bounded side lane. Every error is caught and logged — the
|
|
411
|
+
// settle hook NEVER throws into the event bus — and ownerless
|
|
412
|
+
// conversations (no SessionMeta owner) do nothing.
|
|
413
|
+
opened.events.subscribe((event) => {
|
|
414
|
+
if (event.type !== "run.settled")
|
|
415
|
+
return;
|
|
416
|
+
void opened.eventBus.runBounded("history run.settled", async () => {
|
|
417
|
+
try {
|
|
418
|
+
const meta = await opened.harness.snapshot(SessionMetaDoc, event.conversationId, BACKGROUND_CONTEXT);
|
|
419
|
+
const owner = meta?.owner;
|
|
420
|
+
if (owner === undefined)
|
|
421
|
+
return;
|
|
422
|
+
await opened.history.workspace(owner)?.catchUp({
|
|
423
|
+
actor: { kind: "agent" },
|
|
424
|
+
summary: "agent changes",
|
|
425
|
+
conversationId: event.conversationId,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
console.warn(`workspace history catch-up failed: ${String(error)}`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
});
|
|
345
433
|
// The pipeline is registered FIRST (W1 Task 3): it replaces the naive
|
|
346
434
|
// `X-Sigrun-User` hook — Host/Origin checks, the public surface, the
|
|
347
435
|
// env service token, the setup gate, and method resolution stamp
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The §5.9 job access rule, shared (tabs+skills Task 5): moved OUT of
|
|
3
|
+
* `routes/jobs.ts` unchanged in behaviour so the REST routes and the
|
|
4
|
+
* `core/jobs` view (`core-views/jobs.ts`) resolve the SAME verdicts —
|
|
5
|
+
* hidden is absence/404 everywhere, never a 403 tell; a read-only
|
|
6
|
+
* caller on a write path is the one 403.
|
|
7
|
+
*
|
|
8
|
+
* Two shapes, exactly the routes' old inline logic:
|
|
9
|
+
*
|
|
10
|
+
* - {@link jobAccess} — the access verdict for one LIVE row (the
|
|
11
|
+
* record's owner/sharing through `accessOf`);
|
|
12
|
+
* - {@link resolveJob} — one job resolved for the access rule: the
|
|
13
|
+
* live record's row when it exists; for a RETIRED job (the history
|
|
14
|
+
* routes' case) the newest revision's shape, which answers until
|
|
15
|
+
* retention trims the records. `undefined` only when neither exists.
|
|
16
|
+
*/
|
|
17
|
+
import { accessOf } from "@sigrun/shared/access";
|
|
18
|
+
/** The §5.9 resource form ({owner?, sharing?}) of a job row. */
|
|
19
|
+
function resourceOf(row) {
|
|
20
|
+
return {
|
|
21
|
+
...(row.owner === undefined ? {} : { owner: row.owner }),
|
|
22
|
+
...(row.sharing === undefined ? {} : { sharing: row.sharing }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The caller's access to one LIVE job row (§5.9 `accessOf`). */
|
|
26
|
+
export function jobAccess(principal, row) {
|
|
27
|
+
return accessOf(principal, resourceOf(row));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve one job for the access rule: the live record's row when it
|
|
31
|
+
* exists; for a RETIRED job the newest revision's shape (access is
|
|
32
|
+
* evaluated against that revision's owner/sharing — the record is gone
|
|
33
|
+
* with the retire). `undefined` only when neither exists (unknown id).
|
|
34
|
+
*/
|
|
35
|
+
export async function resolveJob(jobs, principal, id) {
|
|
36
|
+
const wire = await jobs.get(id);
|
|
37
|
+
if (wire !== undefined) {
|
|
38
|
+
const { access: _ignored, ...row } = wire;
|
|
39
|
+
return { row, access: accessOf(principal, resourceOf(row)) };
|
|
40
|
+
}
|
|
41
|
+
// Retired: the newest revision answers until retention trims it.
|
|
42
|
+
const [newest] = await jobs.revisions(id);
|
|
43
|
+
if (newest === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
const row = {
|
|
46
|
+
id,
|
|
47
|
+
name: newest.spec.name,
|
|
48
|
+
source: newest.source,
|
|
49
|
+
...(newest.owner === undefined ? {} : { owner: newest.owner }),
|
|
50
|
+
...(newest.sharing === undefined ? {} : { sharing: newest.sharing }),
|
|
51
|
+
schedule: newest.spec.schedule,
|
|
52
|
+
action: newest.spec.action,
|
|
53
|
+
enabled: newest.enabled,
|
|
54
|
+
version: newest.version,
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
// The retired row's status went with the record; the nudge only
|
|
58
|
+
// needs identity + definition (clients refetch the list).
|
|
59
|
+
row: { ...row, state: "disabled" },
|
|
60
|
+
access: accessOf(principal, resourceOf(row)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -84,20 +84,32 @@ export function startNotifications(ctx) {
|
|
|
84
84
|
? `${config.publicUrl}/#/c/${base.conversationId}`
|
|
85
85
|
: undefined;
|
|
86
86
|
for (const user of recipients) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// Absent optional fields are OMITTED, never null/undefined —
|
|
88
|
+
// the conditional spreads keep the wire shape exact (callId
|
|
89
|
+
// rides ONLY interaction.pending; outcome ONLY run.settled).
|
|
90
|
+
// Build per-variant so the per-recipient event is a complete
|
|
91
|
+
// NotificationEvent with NO widening cast.
|
|
92
|
+
const shared = {
|
|
93
|
+
...(base.conversationId === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { conversationId: base.conversationId }),
|
|
96
|
+
...(base.jobId === undefined ? {} : { jobId: base.jobId }),
|
|
92
97
|
user,
|
|
93
98
|
title: base.title,
|
|
94
99
|
body: base.body,
|
|
95
100
|
...(url === undefined ? {} : { url }),
|
|
96
|
-
// The settled outcome rides ONLY run.settled events — the
|
|
97
|
-
// other variants never gain the field (the type is the union;
|
|
98
|
-
// the conditional spread keeps the wire shape exact).
|
|
99
|
-
...(base.outcome === undefined ? {} : { outcome: base.outcome }),
|
|
100
101
|
};
|
|
102
|
+
const event = base.type === "interaction.pending"
|
|
103
|
+
? {
|
|
104
|
+
type: "interaction.pending",
|
|
105
|
+
...shared,
|
|
106
|
+
...(base.callId === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { callId: base.callId }),
|
|
109
|
+
}
|
|
110
|
+
: base.type === "job.failed"
|
|
111
|
+
? { type: "job.failed", ...shared }
|
|
112
|
+
: { type: "run.settled", ...shared, outcome: base.outcome };
|
|
101
113
|
deliverEvent(ctx, targets, event);
|
|
102
114
|
}
|
|
103
115
|
};
|
|
@@ -157,10 +169,10 @@ export function startNotifications(ctx) {
|
|
|
157
169
|
// second answer read, no inferred status). An unanswered run never
|
|
158
170
|
// titles itself "Reply ready": the reason is the run's own, with
|
|
159
171
|
// the bare status as the fallback for a reason-less settle.
|
|
172
|
+
const reason_ = reason ?? "unanswered";
|
|
160
173
|
const settledOutcome = outcome === "answered"
|
|
161
174
|
? { status: "answered" }
|
|
162
|
-
: { status: "unanswered", reason:
|
|
163
|
-
const settledReason = reason ?? "unanswered";
|
|
175
|
+
: { status: "unanswered", reason: reason_ };
|
|
164
176
|
deliver({
|
|
165
177
|
type: "run.settled",
|
|
166
178
|
conversationId,
|
|
@@ -171,7 +183,7 @@ export function startNotifications(ctx) {
|
|
|
171
183
|
: "Run ended without a reply",
|
|
172
184
|
body: outcome === "answered"
|
|
173
185
|
? displayName(meta, conversationId)
|
|
174
|
-
: `${displayName(meta, conversationId)} — ${
|
|
186
|
+
: `${displayName(meta, conversationId)} — ${reason_}`,
|
|
175
187
|
outcome: settledOutcome,
|
|
176
188
|
}, [meta?.owner]);
|
|
177
189
|
};
|
|
@@ -116,7 +116,15 @@ export function registerExtensionRoutes(app, ctx) {
|
|
|
116
116
|
routes: [],
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
// The core-owned views (tabs+skills Task 4): the reserved `core`
|
|
120
|
+
// slug's view defs under `coreViews` — core views are NOT
|
|
121
|
+
// extensions, so no `extensions[]` row exists for them (the health
|
|
122
|
+
// dot is unaffected). Absent when none attach (sparse).
|
|
123
|
+
const coreViews = ctx.surfaces.coreViews();
|
|
124
|
+
return {
|
|
125
|
+
extensions,
|
|
126
|
+
...(coreViews.length === 0 ? {} : { coreViews }),
|
|
127
|
+
};
|
|
120
128
|
});
|
|
121
129
|
// The per-extension settings object (`settings/<slug>.json`), absent
|
|
122
130
|
// file → `{}`. GET is ungated beyond the pipeline: settings values are
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* Task 3's JobsService.
|
|
4
4
|
*
|
|
5
5
|
* ACCESS (§5.9, the conversations rule applied to job records): every
|
|
6
|
-
* route resolves
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* route resolves the shared rule (`jobs-access.ts` — moved out of this
|
|
7
|
+
* module for tabs+skills Task 5 so the `core/jobs` view resolves the
|
|
8
|
+
* SAME verdicts) against the job RECORD — hidden is 404
|
|
9
|
+
* `job_not_found` on every `:id` route and absence from the list (no
|
|
10
|
+
* 403 tell); a read-only caller on a write route gets 403. The owner is
|
|
11
|
+
* server-stamped at create (`principal.user`; a body-carried `owner` is
|
|
12
|
+
* stripped) and moves only through the transfer path; `sharing` is the
|
|
13
|
+
* PATCH share surface (mirrored onto the record from the merged spec,
|
|
14
|
+
* exactly like a declared job).
|
|
14
15
|
*
|
|
15
16
|
* The HISTORY routes (`runs`/`revisions`) answer for a RETIRED job too,
|
|
16
17
|
* until retention trims its records — access is evaluated against the
|
|
@@ -21,17 +22,10 @@
|
|
|
21
22
|
* return 409 `job_declared` (edit the source instead). Run-now honors
|
|
22
23
|
* the caller's write access and the job's own `overlap`.
|
|
23
24
|
*/
|
|
24
|
-
import { accessOf } from "@sigrun/shared/access";
|
|
25
25
|
import { accessPrincipalOf } from "../auth/access-principal.js";
|
|
26
|
+
import { jobAccess, resolveJob, } from "../jobs-access.js";
|
|
26
27
|
import { JobDeclaredError, JobNotFoundError } from "../../jobs/index.js";
|
|
27
28
|
import { validateJobSpec } from "../../jobs/spec.js";
|
|
28
|
-
/** The §5.9 resource form ({owner?, sharing?}) of a job row. */
|
|
29
|
-
function resourceOf(row) {
|
|
30
|
-
return {
|
|
31
|
-
...(row.owner === undefined ? {} : { owner: row.owner }),
|
|
32
|
-
...(row.sharing === undefined ? {} : { sharing: row.sharing }),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
29
|
/**
|
|
36
30
|
* The shared `:id` error mapping: unknown/hidden → 404 `job_not_found`;
|
|
37
31
|
* visible-but-readonly on a write route → 403 (the callers resolve
|
|
@@ -59,39 +53,12 @@ export function registerJobRoutes(app, ctx) {
|
|
|
59
53
|
const jobs = opened.jobs;
|
|
60
54
|
const accessPrincipal = (request) => accessPrincipalOf(request.principal, opened.identity.users);
|
|
61
55
|
/**
|
|
62
|
-
* Resolve one job for the access rule
|
|
63
|
-
* exists; for a RETIRED job (the history
|
|
64
|
-
* revision's shape. `undefined` only when
|
|
56
|
+
* Resolve one job for the access rule (the shared seam): the live
|
|
57
|
+
* record's row when it exists; for a RETIRED job (the history
|
|
58
|
+
* routes' case) the newest revision's shape. `undefined` only when
|
|
59
|
+
* neither exists (unknown id).
|
|
65
60
|
*/
|
|
66
|
-
const resolve = async (request, id) =>
|
|
67
|
-
const principal = accessPrincipal(request);
|
|
68
|
-
const wire = await jobs.get(id);
|
|
69
|
-
if (wire !== undefined) {
|
|
70
|
-
const { access: _ignored, ...row } = wire;
|
|
71
|
-
return { row, access: accessOf(principal, resourceOf(row)) };
|
|
72
|
-
}
|
|
73
|
-
// Retired: the newest revision answers until retention trims it.
|
|
74
|
-
const [newest] = await jobs.revisions(id);
|
|
75
|
-
if (newest === undefined)
|
|
76
|
-
return undefined;
|
|
77
|
-
const row = {
|
|
78
|
-
id,
|
|
79
|
-
name: newest.spec.name,
|
|
80
|
-
source: newest.source,
|
|
81
|
-
...(newest.owner === undefined ? {} : { owner: newest.owner }),
|
|
82
|
-
...(newest.sharing === undefined ? {} : { sharing: newest.sharing }),
|
|
83
|
-
schedule: newest.spec.schedule,
|
|
84
|
-
action: newest.spec.action,
|
|
85
|
-
enabled: newest.enabled,
|
|
86
|
-
version: newest.version,
|
|
87
|
-
};
|
|
88
|
-
return {
|
|
89
|
-
// The retired row's status went with the record; the nudge only
|
|
90
|
-
// needs identity + definition (clients refetch the list).
|
|
91
|
-
row: { ...row, state: "disabled" },
|
|
92
|
-
access: accessOf(principal, resourceOf(row)),
|
|
93
|
-
};
|
|
94
|
-
};
|
|
61
|
+
const resolve = async (request, id) => await resolveJob(jobs, accessPrincipal(request), id);
|
|
95
62
|
/** The read guard: unknown/hidden → the 404; the row otherwise. */
|
|
96
63
|
const visible = async (request, reply, id) => {
|
|
97
64
|
const resolved = await resolve(request, id);
|
|
@@ -106,7 +73,7 @@ export function registerJobRoutes(app, ctx) {
|
|
|
106
73
|
const principal = accessPrincipal(request);
|
|
107
74
|
const visibleJobs = [];
|
|
108
75
|
for (const row of await jobs.list()) {
|
|
109
|
-
const access =
|
|
76
|
+
const access = jobAccess(principal, row);
|
|
110
77
|
if (access === "none")
|
|
111
78
|
continue;
|
|
112
79
|
visibleJobs.push({ ...row, access });
|
|
@@ -120,7 +87,7 @@ export function registerJobRoutes(app, ctx) {
|
|
|
120
87
|
if (wire === undefined) {
|
|
121
88
|
return await deny(reply, "job_not_found");
|
|
122
89
|
}
|
|
123
|
-
const access =
|
|
90
|
+
const access = jobAccess(accessPrincipal(request), wire);
|
|
124
91
|
if (access === "none") {
|
|
125
92
|
return await deny(reply, "job_not_found");
|
|
126
93
|
}
|
|
@@ -222,7 +189,7 @@ export function registerJobRoutes(app, ctx) {
|
|
|
222
189
|
if (wire === undefined) {
|
|
223
190
|
return await deny(reply, "job_not_found");
|
|
224
191
|
}
|
|
225
|
-
const access =
|
|
192
|
+
const access = jobAccess(accessPrincipal(request), wire);
|
|
226
193
|
if (access === "none") {
|
|
227
194
|
return await deny(reply, "job_not_found");
|
|
228
195
|
}
|
|
@@ -253,7 +220,7 @@ export function registerJobRoutes(app, ctx) {
|
|
|
253
220
|
if (wire === undefined) {
|
|
254
221
|
return await deny(reply, "job_not_found");
|
|
255
222
|
}
|
|
256
|
-
const access =
|
|
223
|
+
const access = jobAccess(accessPrincipal(request), wire);
|
|
257
224
|
if (access === "none") {
|
|
258
225
|
return await deny(reply, "job_not_found");
|
|
259
226
|
}
|
|
@@ -139,6 +139,15 @@ export function registerMetaRoutes(app, ctx) {
|
|
|
139
139
|
: { reason: opened.isolation.reason }),
|
|
140
140
|
osUsers: opened.osUsers.status,
|
|
141
141
|
},
|
|
142
|
+
// The core-owned views (tabs+skills Task 4): the reserved `core`
|
|
143
|
+
// slug's view defs, projected per request from the surface service
|
|
144
|
+
// (the SAME list /v1/extensions serves) — the tab shell's rail
|
|
145
|
+
// reads them from here and never fetches /v1/extensions separately.
|
|
146
|
+
// Absent when no core views attached (sparse — Task 4 boots with an
|
|
147
|
+
// empty list, Tasks 5/12 attach the real views).
|
|
148
|
+
...(ctx.surfaces.coreViews().length === 0
|
|
149
|
+
? {}
|
|
150
|
+
: { coreViews: ctx.surfaces.coreViews() }),
|
|
142
151
|
// The seam inventory: declaration-derived, no doc reads.
|
|
143
152
|
seam: {
|
|
144
153
|
interactions: [...opened.seamKinds]
|
|
@@ -111,6 +111,15 @@ function resolveProxyIdentity(request, ctx) {
|
|
|
111
111
|
...(email === undefined ? {} : { email }),
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
/** The accepted claim-body keys, in the validator's switch order. */
|
|
115
|
+
const CLAIM_BODY_FIELDS = [
|
|
116
|
+
"code",
|
|
117
|
+
"user",
|
|
118
|
+
"name",
|
|
119
|
+
"email",
|
|
120
|
+
"issueToken",
|
|
121
|
+
"tokenName",
|
|
122
|
+
];
|
|
114
123
|
/**
|
|
115
124
|
* Validate the claim body strictly: unknown keys and wrong types are
|
|
116
125
|
* client errors (the repo's fail-loud validator posture).
|
|
@@ -141,7 +150,10 @@ function validateClaimBody(body) {
|
|
|
141
150
|
result.issueToken = value;
|
|
142
151
|
break;
|
|
143
152
|
default:
|
|
144
|
-
|
|
153
|
+
// Name BOTH directions: the sent key (field) and the accepted
|
|
154
|
+
// set (expected) — a `username` guess must point at `user`,
|
|
155
|
+
// never leave the operator to guess (fresh-eyes A3).
|
|
156
|
+
return { error: "invalid_field", field: key, expected: CLAIM_BODY_FIELDS };
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
159
|
return result;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { rewriteImages } from "../images/rewrite.js";
|
|
28
28
|
import { validateTree } from "../../blocks/validate.js";
|
|
29
|
+
import { coreViewInfos } from "../core-views/index.js";
|
|
29
30
|
import { AccessCache } from "./access-cache.js";
|
|
30
31
|
import { ActionDedup } from "./dedup.js";
|
|
31
32
|
import { eventKey, instanceKey as defaultInstanceKey } from "./instance.js";
|
|
@@ -148,6 +149,43 @@ export class SurfaceService {
|
|
|
148
149
|
this.publishChanged = options?.publishChanged;
|
|
149
150
|
this.imageSign = options?.imageSign;
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Attach the core-owned views (tabs+skills Task 4): registered under
|
|
154
|
+
* the reserved `core` slug through the same `extra` lookup the
|
|
155
|
+
* settings surfaces use, so core views render/data/act through the
|
|
156
|
+
* ONE code path extension surfaces do. Their defs are retained for
|
|
157
|
+
* the `/v1/meta` + `/v1/extensions` `coreViews` projection (core
|
|
158
|
+
* views are NOT extensions — no `extensions[]` row, so the health
|
|
159
|
+
* dot is unaffected).
|
|
160
|
+
*/
|
|
161
|
+
attachCoreViews(defs) {
|
|
162
|
+
this.extra.push({ extension: "core", defs });
|
|
163
|
+
this.coreViewDefs.push(...defs);
|
|
164
|
+
}
|
|
165
|
+
/** The attached core-view defs (attachCoreViews order). */
|
|
166
|
+
coreViewDefs = [];
|
|
167
|
+
/**
|
|
168
|
+
* The `coreViews` wire rows (tabs+skills Task 4): id/title/icon (+
|
|
169
|
+
* refreshMs) per attached core-view def.
|
|
170
|
+
*/
|
|
171
|
+
coreViews() {
|
|
172
|
+
return coreViewInfos(this.coreViewDefs);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Bump every KNOWN instance of one surface (tabs+skills Task 4): the
|
|
176
|
+
* instance table's keys are `slug\u0001id\u0001key`, so a prefix scan
|
|
177
|
+
* finds every rendered instance; each gets a full {@link bumpRev}
|
|
178
|
+
* (rev advance + `surface.changed` publication). A surface with NO
|
|
179
|
+
* rendered instances is a no-op (nothing watches it).
|
|
180
|
+
*/
|
|
181
|
+
bumpAllInstances(slug, id) {
|
|
182
|
+
const prefix = `${slug}\u0001${id}\u0001`;
|
|
183
|
+
for (const composite of this.instances.keys()) {
|
|
184
|
+
if (!composite.startsWith(prefix))
|
|
185
|
+
continue;
|
|
186
|
+
this.bumpRev(slug, id, composite.slice(prefix.length));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
151
189
|
/**
|
|
152
190
|
* Attach the settings surfaces post-construction (their save
|
|
153
191
|
* handlers close over THIS service for the optimistic-concurrency
|
package/dist/skills/index.js
CHANGED
|
@@ -111,7 +111,7 @@ function unquote(value) {
|
|
|
111
111
|
* closing delimiter. Returns `undefined` when the block is absent or never
|
|
112
112
|
* closes (neither required field could have parsed).
|
|
113
113
|
*/
|
|
114
|
-
function parseSkillFile(text) {
|
|
114
|
+
export function parseSkillFile(text) {
|
|
115
115
|
const lines = text.split(/\r?\n/);
|
|
116
116
|
if (lines[0]?.trim() !== FRONTMATTER_DELIMITER)
|
|
117
117
|
return undefined;
|