@sigrun/sigrun 0.4.2 → 0.6.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 +92 -1
- package/README.md +63 -33
- package/dist/commands/expand.js +37 -5
- package/dist/curation/attribution.js +135 -0
- package/dist/curation/deletion.js +32 -0
- package/dist/curation/gate.js +50 -0
- package/dist/curation/host.js +342 -0
- package/dist/curation/internal.js +54 -0
- package/dist/curation/names.js +5 -0
- package/dist/curation/overlays.js +325 -0
- package/dist/curation/paths.js +17 -0
- package/dist/curation/run-doc.js +64 -0
- package/dist/curation/runs.js +579 -0
- package/dist/curation/scan.js +30 -0
- package/dist/curation/skill-tool.js +308 -0
- package/dist/curation/store.js +444 -0
- package/dist/curation/targets.js +77 -0
- package/dist/curation/usage.js +124 -0
- package/dist/curation/wrap.js +212 -0
- package/dist/extension.d.ts +336 -1
- package/dist/harness/boot.js +236 -13
- package/dist/harness/builtins.js +19 -12
- package/dist/harness/event-bus.js +64 -0
- package/dist/harness/identity/primary-admin.js +19 -0
- package/dist/harness/loader.js +114 -1
- package/dist/harness/naming.js +6 -0
- package/dist/harness/packages.js +14 -9
- package/dist/harness/reserved-sections.js +4 -0
- package/dist/harness/seam-host.js +154 -26
- package/dist/harness/sections/context-files.js +6 -3
- package/dist/harness/sections/job-state.js +24 -0
- package/dist/harness/settings.js +2 -2
- package/dist/harness/tools.js +70 -0
- package/dist/harness/workspace.js +30 -3
- package/dist/jobs/fire.js +37 -14
- package/dist/jobs/index.js +34 -7
- package/dist/jobs/reconcile.js +9 -0
- package/dist/jobs/skills-refs.js +40 -0
- package/dist/jobs/state-dir.js +16 -0
- package/dist/jobs/transfer.js +14 -0
- package/dist/sandbox/policy.js +27 -4
- package/dist/serve.js +5 -3
- package/dist/server/blocks/answers.js +9 -3
- package/dist/server/conversation-ops.js +34 -3
- package/dist/server/helpers.js +1 -1
- package/dist/server/host-bridge.js +31 -1
- package/dist/server/notifications.js +108 -16
- package/dist/server/routes/jobs.js +31 -0
- package/dist/server/routes/meta.js +8 -3
- package/dist/server/routes/submissions.js +5 -0
- package/dist/server/routes/surfaces.js +3 -1
- package/dist/server/routes/users.js +7 -0
- package/dist/server/schema-fixture.js +168 -0
- package/dist/server/session-meta.js +12 -10
- package/dist/settings/core-sections.js +18 -0
- package/dist/settings/registry.js +12 -0
- package/dist/skills/index.js +88 -19
- package/node_modules/@sigrun/shared/CHANGELOG.md +6 -1
- package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
- package/node_modules/@sigrun/shared/dist/access.js +8 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
- package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +9 -7
- package/webui/dist/assets/index-BR0di0OG.css +1 -0
- package/webui/dist/assets/{index-DWmsAQl2.js → index-BzrA1T4g.js} +5 -5
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-h7zDY9vD.css +0 -1
|
@@ -20,6 +20,7 @@ import { AgentDoc, configure } from "@earendil-works/pi-durable";
|
|
|
20
20
|
import { accessOf, isSharing, } from "@sigrun/shared/access";
|
|
21
21
|
import { extname } from "node:path";
|
|
22
22
|
import { touchActivity } from "../harness/activity.js";
|
|
23
|
+
import { ensureRequestId, recordAuthor, } from "../curation/attribution.js";
|
|
23
24
|
import { resolveServedModelRefreshing } from "../harness/providers/refresh.js";
|
|
24
25
|
import { isSuperuser } from "./auth/principal.js";
|
|
25
26
|
import { isConfinedPath, parseSubmissionInput } from "./content-parts.js";
|
|
@@ -145,6 +146,17 @@ export async function createConversationOp(ctx, input = {}) {
|
|
|
145
146
|
if (input.name !== undefined && typeof input.name !== "string") {
|
|
146
147
|
throw new ConversationOpError({ error: "invalid_field", field: "name" });
|
|
147
148
|
}
|
|
149
|
+
if (input.job !== undefined) {
|
|
150
|
+
// Resolve the live job (and therefore its EFFECTIVE owner) before the
|
|
151
|
+
// createConversation table write or the session-meta commit. An absent
|
|
152
|
+
// requested owner and an absent effective owner are an exact match.
|
|
153
|
+
const job = typeof input.job === "string"
|
|
154
|
+
? await opened.jobs.get(input.job)
|
|
155
|
+
: undefined;
|
|
156
|
+
if (job === undefined || input.owner !== job.owner) {
|
|
157
|
+
throw new ConversationOpError({ error: "invalid_field", field: "job" });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
148
160
|
if (input.source !== undefined && typeof input.source !== "string") {
|
|
149
161
|
throw new ConversationOpError({ error: "invalid_field", field: "source" });
|
|
150
162
|
}
|
|
@@ -226,6 +238,8 @@ export async function createConversationOp(ctx, input = {}) {
|
|
|
226
238
|
}
|
|
227
239
|
if (input.name !== undefined)
|
|
228
240
|
draft.name = input.name;
|
|
241
|
+
if (input.job !== undefined)
|
|
242
|
+
draft.jobId = input.job;
|
|
229
243
|
if (input.source !== undefined)
|
|
230
244
|
draft.source = input.source;
|
|
231
245
|
await opened.rewriteUserSelection(tx, conversation.id, undefined, input.owner);
|
|
@@ -247,6 +261,9 @@ export async function submitOp(ctx, conversation, input, opts = {}) {
|
|
|
247
261
|
if (!parsed.ok) {
|
|
248
262
|
throw new ConversationOpError({ error: "invalid_input" });
|
|
249
263
|
}
|
|
264
|
+
// Every input carries a requestId (curator P1): the caller's own id, or
|
|
265
|
+
// a generated `sigrun:` UUID — the pre-admission attribution key.
|
|
266
|
+
const requestId = ensureRequestId(opts.requestId);
|
|
250
267
|
// Spec §2/§3 binding contract: a conversation pinned to an unserved
|
|
251
268
|
// model fails explicitly AT SUBMIT — 4xx with the available ids —
|
|
252
269
|
// never a silent fallback, and never a settled no_model run surfaced
|
|
@@ -316,7 +333,11 @@ export async function submitOp(ctx, conversation, input, opts = {}) {
|
|
|
316
333
|
else {
|
|
317
334
|
try {
|
|
318
335
|
const env = await envThunk();
|
|
319
|
-
|
|
336
|
+
// Curator A3: the learned root resolves through the conversation
|
|
337
|
+
// OWNER (SessionMetaDoc), not the submitting user — a shared-write
|
|
338
|
+
// submitter expands against the owner's learned skills.
|
|
339
|
+
const meta = await opened.harness.snapshot(SessionMetaDoc, conversation.id, BACKGROUND_CONTEXT);
|
|
340
|
+
expansion = await expandInput(parsed.parts, opened.commandExpansion, env, BACKGROUND_CONTEXT, meta?.owner);
|
|
320
341
|
}
|
|
321
342
|
catch (error) {
|
|
322
343
|
if (error instanceof CommandUsageError) {
|
|
@@ -356,13 +377,20 @@ export async function submitOp(ctx, conversation, input, opts = {}) {
|
|
|
356
377
|
const draft = {
|
|
357
378
|
type: "input",
|
|
358
379
|
content,
|
|
359
|
-
|
|
380
|
+
requestId,
|
|
360
381
|
...(whenBusy === undefined ? {} : { whenBusy: whenBusy }),
|
|
361
382
|
...(entry === undefined ? {} : { entry }),
|
|
362
383
|
};
|
|
384
|
+
// Attribution (curator P1), two writes per input: `byRequest` BEFORE the
|
|
385
|
+
// admission (the requestId exists pre-admission, so this never races; an
|
|
386
|
+
// orphan record from a failed submit is harmless) and `bySubmission`
|
|
387
|
+
// right after it returns — the key the live run's input ids resolve
|
|
388
|
+
// against. An `undefined` user records nothing (unattributed).
|
|
389
|
+
await recordAuthor(conversation, { requestId }, opts.user, BACKGROUND_CONTEXT);
|
|
363
390
|
// The submit pass-through is retired (§6.7): the memory seam's
|
|
364
391
|
// runSettled owns capture, so submissions admit directly.
|
|
365
392
|
const submission = await conversation.submit(draft, BACKGROUND_CONTEXT);
|
|
393
|
+
await recordAuthor(conversation, { submissionId: submission.id }, opts.user, BACKGROUND_CONTEXT);
|
|
366
394
|
// Admission stamps activity: the sidebar's activity sort must move the
|
|
367
395
|
// moment a prompt lands — before the run settles.
|
|
368
396
|
await touchActivity(opened.harness, conversation.id, opened.now, BACKGROUND_CONTEXT);
|
|
@@ -394,7 +422,10 @@ export async function listConversationsOp(ctx, caller, filters = {}) {
|
|
|
394
422
|
// flags, and the create source (spec §9.5).
|
|
395
423
|
const meta = await opened.harness.snapshot(SessionMetaDoc, record.id, BACKGROUND_CONTEXT);
|
|
396
424
|
const access = accessOf(caller, meta ?? {});
|
|
397
|
-
// Hidden is absent from the list (spec §5.9)
|
|
425
|
+
// Hidden is absent from the list (spec §5.9) — and internal means
|
|
426
|
+
// hidden for EVERY caller (curator P5, accessOf rule 0): the
|
|
427
|
+
// superuser and the bridge's trusted read are covered by the same
|
|
428
|
+
// line, so no caller-specific skip can drift.
|
|
398
429
|
if (access === "none")
|
|
399
430
|
continue;
|
|
400
431
|
// The archived filter (absent = all).
|
package/dist/server/helpers.js
CHANGED
|
@@ -86,7 +86,7 @@ function autoEnableRemovals(settings, env) {
|
|
|
86
86
|
const server = entry;
|
|
87
87
|
if (server.enabled === false || server.disabled === true)
|
|
88
88
|
continue;
|
|
89
|
-
// @sigrun/ext-mcp's server default is "codemode"; its
|
|
89
|
+
// @sigrun/ext-mcp's server default is "codemode"; its deprecated
|
|
90
90
|
// alias `codemode-deferred` normalizes to "codemode".
|
|
91
91
|
const exposure = server.exposure === "codemode-deferred"
|
|
92
92
|
? "codemode"
|
|
@@ -22,6 +22,7 @@ import { userWire } from "./routes/users.js";
|
|
|
22
22
|
import { accessPrincipalOf } from "./auth/access-principal.js";
|
|
23
23
|
import { abortOp, compactOp, configureOp, ConversationOpError, createConversationOp, forkOp, listConversationsOp, patchMetaOp, readFileOp, readMetaOp, submitOp, uploadOp, } from "./conversation-ops.js";
|
|
24
24
|
import { listModels } from "./helpers.js";
|
|
25
|
+
import { notifyExtension } from "./notifications.js";
|
|
25
26
|
import { SessionMetaDoc } from "./session-meta.js";
|
|
26
27
|
import { accountLinkWire, serializeConversationView } from "./views.js";
|
|
27
28
|
/**
|
|
@@ -59,11 +60,13 @@ export function createHostBridge(ctx) {
|
|
|
59
60
|
const conversation = await conversationOf(id);
|
|
60
61
|
// `user` is accepted and NOT verified: extensions own their
|
|
61
62
|
// user mapping (spec §9.5) — a channel's unmapped user submits
|
|
62
|
-
// fine, and owner attribution happened at create.
|
|
63
|
+
// fine, and owner attribution happened at create. P1 records
|
|
64
|
+
// it verbatim as the input's author.
|
|
63
65
|
return await submitOp(ctx, conversation, input, {
|
|
64
66
|
...(opts?.requestId === undefined
|
|
65
67
|
? {}
|
|
66
68
|
: { requestId: opts.requestId }),
|
|
69
|
+
...(opts?.user === undefined ? {} : { user: opts.user }),
|
|
67
70
|
...(opts?.whenBusy === undefined
|
|
68
71
|
? {}
|
|
69
72
|
: { whenBusy: opts.whenBusy }),
|
|
@@ -106,6 +109,15 @@ export function createHostBridge(ctx) {
|
|
|
106
109
|
await conversationOf(id);
|
|
107
110
|
return await readMetaOp(opened, id);
|
|
108
111
|
},
|
|
112
|
+
async entries(id, query, limit, cursor) {
|
|
113
|
+
const conversation = await conversationOf(id);
|
|
114
|
+
return await conversation.entries(query, limit, cursor, BACKGROUND_CONTEXT);
|
|
115
|
+
},
|
|
116
|
+
async entry(id, entryId) {
|
|
117
|
+
const conversation = await conversationOf(id);
|
|
118
|
+
const page = await conversation.entries({ minEntryId: entryId, maxEntryId: entryId }, 1, undefined, BACKGROUND_CONTEXT);
|
|
119
|
+
return page.items[0];
|
|
120
|
+
},
|
|
109
121
|
// Gateway C4a: the remaining conversation ops, each the SAME op
|
|
110
122
|
// its HTTP route runs — resolved through conversationOf (unknown id
|
|
111
123
|
// → the bridge's loud `conversation <id> not found`).
|
|
@@ -168,6 +180,20 @@ export function createHostBridge(ctx) {
|
|
|
168
180
|
return await readFileOp(ctx, conversation, path);
|
|
169
181
|
},
|
|
170
182
|
},
|
|
183
|
+
jobs: {
|
|
184
|
+
async get(id) {
|
|
185
|
+
const job = await opened.jobs.get(id);
|
|
186
|
+
if (job === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
return {
|
|
189
|
+
id: job.id,
|
|
190
|
+
name: job.name,
|
|
191
|
+
...(job.owner === undefined ? {} : { owner: job.owner }),
|
|
192
|
+
enabled: job.enabled,
|
|
193
|
+
state: job.state,
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
},
|
|
171
197
|
users: {
|
|
172
198
|
// The directory in wire form (emails are admin material; the
|
|
173
199
|
// bridge is in-process server code, the same trust as the
|
|
@@ -277,6 +303,7 @@ export function createHostBridge(ctx) {
|
|
|
277
303
|
: { notice: result.notice };
|
|
278
304
|
},
|
|
279
305
|
},
|
|
306
|
+
notify: (slug, input) => notifyExtension(ctx, slug, input),
|
|
280
307
|
surfaces: {
|
|
281
308
|
// UI-blocks U3 Task 4 (spec §7.3): the same bumpRev the actions
|
|
282
309
|
// route takes — the rev advance and the hub event are one step.
|
|
@@ -284,5 +311,8 @@ export function createHostBridge(ctx) {
|
|
|
284
311
|
ctx.surfaces.bumpRev(slug, id, key, update);
|
|
285
312
|
},
|
|
286
313
|
},
|
|
314
|
+
// Curator P6 (A9): the boot-built curation host, verbatim — the
|
|
315
|
+
// ONE instance every seam host reaches through this bridge.
|
|
316
|
+
curation: ctx.opened.curationHost,
|
|
287
317
|
};
|
|
288
318
|
}
|
|
@@ -40,6 +40,29 @@ import { SessionMetaDoc } from "./session-meta.js";
|
|
|
40
40
|
function displayName(meta, id) {
|
|
41
41
|
return meta?.name ?? meta?.autoName ?? `Conversation ${id}`;
|
|
42
42
|
}
|
|
43
|
+
/** Collect every loaded notifier with its contributing extension's host. */
|
|
44
|
+
export function collectTargets(ctx) {
|
|
45
|
+
const targets = [];
|
|
46
|
+
for (const binding of ctx.opened.notifierBindings) {
|
|
47
|
+
// The Task 2 ext-route host semantics: the captured seam host,
|
|
48
|
+
// cast across the not-yet-bridged members.
|
|
49
|
+
const host = ctx.opened.seamHosts.get(binding.extension);
|
|
50
|
+
for (const notifier of binding.notifiers) {
|
|
51
|
+
targets.push({ extension: binding.extension, notifier, host });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return targets;
|
|
55
|
+
}
|
|
56
|
+
/** Enqueue one per-recipient event through every notifier (never awaited). */
|
|
57
|
+
export function deliverEvent(ctx, targets, event) {
|
|
58
|
+
for (const target of targets) {
|
|
59
|
+
// Fire-and-forget per transport call: one notifier's failure or
|
|
60
|
+
// hang neither blocks nor loses the others' deliveries.
|
|
61
|
+
void ctx.opened.eventBus.runBounded(`notifier ${target.extension}.${target.notifier.name}`, () => target.notifier.notify(event, {
|
|
62
|
+
host: target.host,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
43
66
|
/**
|
|
44
67
|
* Start the notifications surface. Returns its unsubscribe (the server
|
|
45
68
|
* never calls it today — the surface rides the process lifetime — but
|
|
@@ -48,15 +71,7 @@ function displayName(meta, id) {
|
|
|
48
71
|
*/
|
|
49
72
|
export function startNotifications(ctx) {
|
|
50
73
|
const { opened, config } = ctx;
|
|
51
|
-
const targets =
|
|
52
|
-
for (const binding of opened.notifierBindings) {
|
|
53
|
-
// The Task 2 ext-route host semantics: the captured seam host,
|
|
54
|
-
// cast across the not-yet-bridged members.
|
|
55
|
-
const host = opened.seamHosts.get(binding.extension);
|
|
56
|
-
for (const notifier of binding.notifiers) {
|
|
57
|
-
targets.push({ extension: binding.extension, notifier, host });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
74
|
+
const targets = collectTargets(ctx);
|
|
60
75
|
if (targets.length === 0)
|
|
61
76
|
return () => { };
|
|
62
77
|
const bus = opened.eventBus;
|
|
@@ -83,13 +98,7 @@ export function startNotifications(ctx) {
|
|
|
83
98
|
// the conditional spread keeps the wire shape exact).
|
|
84
99
|
...(base.outcome === undefined ? {} : { outcome: base.outcome }),
|
|
85
100
|
};
|
|
86
|
-
|
|
87
|
-
// Fire-and-forget per transport call: one notifier's failure or
|
|
88
|
-
// hang neither blocks nor loses the others' deliveries.
|
|
89
|
-
void bus.runBounded(`notifier ${target.extension}.${target.notifier.name}`, () => target.notifier.notify(event, {
|
|
90
|
-
host: target.host,
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
101
|
+
deliverEvent(ctx, targets, event);
|
|
93
102
|
}
|
|
94
103
|
};
|
|
95
104
|
/** One conversation's live SessionMeta (undefined on absence or read failure, logged). */
|
|
@@ -132,6 +141,12 @@ export function startNotifications(ctx) {
|
|
|
132
141
|
if (mode === "never")
|
|
133
142
|
return;
|
|
134
143
|
const meta = await metaOf(conversationId);
|
|
144
|
+
// Curator P5: an internal run never notifies (no "Reply ready" for
|
|
145
|
+
// curator work). The bus already withholds the host event; this is
|
|
146
|
+
// the belt-and-braces read-side gate (a hand-published or future
|
|
147
|
+
// emit path cannot leak the run).
|
|
148
|
+
if (meta?.internal === true)
|
|
149
|
+
return;
|
|
135
150
|
if (mode === "cron" && meta?.source !== "cron")
|
|
136
151
|
return;
|
|
137
152
|
if (mode === "unwatched" &&
|
|
@@ -204,3 +219,80 @@ export function startNotifications(ctx) {
|
|
|
204
219
|
});
|
|
205
220
|
return unsubscribe;
|
|
206
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Validate and enqueue one extension notice. The caller's promise resolves
|
|
224
|
+
* after validation and enqueueing, not after any transport has delivered.
|
|
225
|
+
*/
|
|
226
|
+
export async function notifyExtension(ctx, slug, input) {
|
|
227
|
+
const users = ctx.opened.identity.users;
|
|
228
|
+
if (users.isClaimed()) {
|
|
229
|
+
if (input.user === undefined) {
|
|
230
|
+
throw new Error("notification user is required when the directory is non-empty");
|
|
231
|
+
}
|
|
232
|
+
if (users.get(input.user) === undefined) {
|
|
233
|
+
throw new Error(`unknown user "${input.user}"`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (typeof input.title !== "string" ||
|
|
237
|
+
input.title.length < 1 ||
|
|
238
|
+
input.title.length > 200) {
|
|
239
|
+
throw new Error("notification title must be 1..200 characters");
|
|
240
|
+
}
|
|
241
|
+
if (typeof input.body !== "string" ||
|
|
242
|
+
input.body.length > 2000) {
|
|
243
|
+
throw new Error("notification body must be 0..2000 characters");
|
|
244
|
+
}
|
|
245
|
+
if (!/^[a-z0-9.-]{1,64}$/.test(input.kind)) {
|
|
246
|
+
throw new Error("notification kind must be 1..64 characters from [a-z0-9.-]");
|
|
247
|
+
}
|
|
248
|
+
if (input.target !== undefined) {
|
|
249
|
+
if (typeof input.target !== "object" || input.target === null || Array.isArray(input.target)) {
|
|
250
|
+
throw new Error("notification target must be an object");
|
|
251
|
+
}
|
|
252
|
+
const hasView = "view" in input.target;
|
|
253
|
+
const hasConversation = "conversation" in input.target;
|
|
254
|
+
if (hasView === hasConversation) {
|
|
255
|
+
throw new Error("notification target must have exactly one of view or conversation");
|
|
256
|
+
}
|
|
257
|
+
if ((hasView && typeof input.target.view !== "string") ||
|
|
258
|
+
(hasConversation && typeof input.target.conversation !== "number")) {
|
|
259
|
+
throw new Error("notification target requires a string view or a number conversation");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let conversationId;
|
|
263
|
+
let url;
|
|
264
|
+
if (input.target !== undefined && "view" in input.target) {
|
|
265
|
+
const view = input.target.view;
|
|
266
|
+
const binding = ctx.opened.extensionSurfaces.find((candidate) => candidate.extension === slug);
|
|
267
|
+
const declared = binding?.defs.some((def) => def.placement === "view" && def.id === view);
|
|
268
|
+
if (declared !== true) {
|
|
269
|
+
throw new Error(`notification view "${view}" was not declared by extension "${slug}"`);
|
|
270
|
+
}
|
|
271
|
+
if (ctx.config.publicUrl !== undefined) {
|
|
272
|
+
const base = `${ctx.config.publicUrl}/#/ext/${encodeURIComponent(slug)}/` +
|
|
273
|
+
encodeURIComponent(view);
|
|
274
|
+
const params = input.target.params ?? {};
|
|
275
|
+
url = Object.keys(params).length === 0
|
|
276
|
+
? base
|
|
277
|
+
: `${base}?params=${encodeURIComponent(JSON.stringify(params))}`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
conversationId = input.target?.conversation;
|
|
282
|
+
if (conversationId !== undefined && ctx.config.publicUrl !== undefined) {
|
|
283
|
+
url = `${ctx.config.publicUrl}/#/c/${conversationId}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const event = {
|
|
287
|
+
type: "extension",
|
|
288
|
+
extension: slug,
|
|
289
|
+
kind: input.kind,
|
|
290
|
+
conversationId,
|
|
291
|
+
jobId: undefined,
|
|
292
|
+
user: input.user,
|
|
293
|
+
title: input.title,
|
|
294
|
+
body: input.body,
|
|
295
|
+
...(url === undefined ? {} : { url }),
|
|
296
|
+
};
|
|
297
|
+
deliverEvent(ctx, collectTargets(ctx), event);
|
|
298
|
+
}
|
|
@@ -212,6 +212,37 @@ export function registerJobRoutes(app, ctx) {
|
|
|
212
212
|
return await domainError(reply, error);
|
|
213
213
|
}
|
|
214
214
|
});
|
|
215
|
+
// POST /v1/jobs/:id/state/reset — write access; any live job source.
|
|
216
|
+
// The job's definition and history stay intact; only its persistent
|
|
217
|
+
// state folder is removed. Unlike delete/transfer/reconcile cleanup,
|
|
218
|
+
// an explicit reset surfaces a failed removal to the caller.
|
|
219
|
+
app.post("/v1/jobs/:id/state/reset", async (request, reply) => {
|
|
220
|
+
const { id } = request.params;
|
|
221
|
+
const wire = await jobs.get(id);
|
|
222
|
+
if (wire === undefined) {
|
|
223
|
+
return await deny(reply, "job_not_found");
|
|
224
|
+
}
|
|
225
|
+
const access = accessOf(accessPrincipal(request), resourceOf(wire));
|
|
226
|
+
if (access === "none") {
|
|
227
|
+
return await deny(reply, "job_not_found");
|
|
228
|
+
}
|
|
229
|
+
if (access === "read") {
|
|
230
|
+
return await deny(reply, "forbidden");
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await jobs.resetState(id);
|
|
234
|
+
return await reply.code(204).send();
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (error instanceof JobNotFoundError) {
|
|
238
|
+
return await deny(reply, "job_not_found");
|
|
239
|
+
}
|
|
240
|
+
return await reply.code(500).send({
|
|
241
|
+
error: "state_reset_failed",
|
|
242
|
+
message: error instanceof Error ? error.message : String(error),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
215
246
|
// POST /v1/jobs/:id/run — write access; fires now (overlap honored).
|
|
216
247
|
// A LIVE record is required (like PATCH/DELETE): a retired job's
|
|
217
248
|
// revision never satisfies run-now — 404, never fireJob's blocked-200
|
|
@@ -178,8 +178,8 @@ export function registerMetaRoutes(app, ctx) {
|
|
|
178
178
|
// templates appear the moment their files land, no reload command. The
|
|
179
179
|
// core + extension halves remain boot constants; see `commands.ts` for
|
|
180
180
|
// the scope rulings.
|
|
181
|
-
app.get("/v1/commands", async () => {
|
|
182
|
-
// The workspace root (the first of the
|
|
181
|
+
app.get("/v1/commands", async (request) => {
|
|
182
|
+
// The workspace root (the first of the four) reads through a
|
|
183
183
|
// workspace env, exactly like skill discovery at generation time;
|
|
184
184
|
// the catalog scan uses the root conversation's env — the static
|
|
185
185
|
// workspace every conversation resolves to today.
|
|
@@ -188,7 +188,12 @@ export function registerMetaRoutes(app, ctx) {
|
|
|
188
188
|
if (env === undefined) {
|
|
189
189
|
throw new Error("envBuilder returned no ExecutionEnv");
|
|
190
190
|
}
|
|
191
|
-
|
|
191
|
+
// Curator A3: learned skills are the CALLER's — a user's `/skill:`
|
|
192
|
+
// menu shows their own learned skills and nobody else's (an
|
|
193
|
+
// unidentified caller sees none).
|
|
194
|
+
const skills = await listSkills(env, BACKGROUND_CONTEXT, expansion.skills, {
|
|
195
|
+
owner: request.principal.user,
|
|
196
|
+
});
|
|
192
197
|
const templates = expandableTemplates(await discoverTemplates(env, BACKGROUND_CONTEXT, expansion.templates), expansion.commandNames());
|
|
193
198
|
return {
|
|
194
199
|
commands: [
|
|
@@ -46,6 +46,11 @@ export function registerSubmissionRoutes(app, ctx) {
|
|
|
46
46
|
...(body.requestId === undefined
|
|
47
47
|
? {}
|
|
48
48
|
: { requestId: String(body.requestId) }),
|
|
49
|
+
// Attribution (curator P1): the resolved principal's user is
|
|
50
|
+
// the input's author; an unidentified caller records nothing.
|
|
51
|
+
...(request.principal.user === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { user: request.principal.user }),
|
|
49
54
|
...(body.whenBusy === undefined ? {} : { whenBusy: body.whenBusy }),
|
|
50
55
|
...(request.query?.waitMs ===
|
|
51
56
|
undefined
|
|
@@ -508,7 +508,9 @@ export function registerSurfaceRoutes(app, ctx) {
|
|
|
508
508
|
if (chatActionStartsTurn(block.tree, body.action)) {
|
|
509
509
|
const answerPayload = (body.payload ?? null);
|
|
510
510
|
const wrapped = { ok: true, rev: block.state.rev };
|
|
511
|
-
await answerBlock(ctx, id, entryId, body.action, answerPayload, actionId
|
|
511
|
+
await answerBlock(ctx, id, entryId, body.action, answerPayload, actionId,
|
|
512
|
+
// The acting user authors the answer turn (curator P1).
|
|
513
|
+
request.principal.user);
|
|
512
514
|
if (actionId !== undefined) {
|
|
513
515
|
ctx.surfaces.chatDedupRecord(actionId, address, fingerprint, wrapped);
|
|
514
516
|
}
|
|
@@ -16,6 +16,7 @@ import { withClaimLock } from "../auth/claim-lock.js";
|
|
|
16
16
|
import { effectiveAdmin, storedGroups } from "../../harness/identity/groups.js";
|
|
17
17
|
import { transferOwnership } from "../ownership.js";
|
|
18
18
|
import { validateGrantPath } from "../../sandbox/grants.js";
|
|
19
|
+
import { removeUserCuration } from "../../curation/deletion.js";
|
|
19
20
|
/** The wire form of one directory user. `admin` stays the STORED flag;
|
|
20
21
|
* `groupAdmin` is DERIVED from stored groups and retained for one release
|
|
21
22
|
* so the users UI keeps rendering. */
|
|
@@ -265,6 +266,12 @@ export function registerUserRoutes(app, ctx) {
|
|
|
265
266
|
// §10.6 step 5: data/state stay for the recipient; core owns only
|
|
266
267
|
// the departing user's credentials, settings, and every cache slug.
|
|
267
268
|
await ctx.opened.extensionStores.removeUserData(user);
|
|
269
|
+
// Curator A10 (P2): the departing user's learned skills and curation
|
|
270
|
+
// state are REMOVED, never transferred — a skill distilled from
|
|
271
|
+
// their conversations can carry their paths and preferences, so it
|
|
272
|
+
// must not load for the recipient. In-flight curator runs fail
|
|
273
|
+
// closed through the stores' user-exists gate (nothing recreated).
|
|
274
|
+
await removeUserCuration(ctx.managedPaths.userAreas(user));
|
|
268
275
|
return await reply.code(204).send();
|
|
269
276
|
});
|
|
270
277
|
}
|
|
@@ -336,6 +336,174 @@ export const VIEW_SCHEMA = {
|
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
},
|
|
339
|
+
{
|
|
340
|
+
"kind": "object",
|
|
341
|
+
"fields": {
|
|
342
|
+
"name": {
|
|
343
|
+
"kind": "string"
|
|
344
|
+
},
|
|
345
|
+
"description": {
|
|
346
|
+
"kind": "string"
|
|
347
|
+
},
|
|
348
|
+
"parameters": {
|
|
349
|
+
"kind": "object",
|
|
350
|
+
"fields": {
|
|
351
|
+
"type": {
|
|
352
|
+
"kind": "string"
|
|
353
|
+
},
|
|
354
|
+
"required": {
|
|
355
|
+
"kind": "array",
|
|
356
|
+
"variants": [
|
|
357
|
+
{
|
|
358
|
+
"kind": "string"
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
"properties": {
|
|
363
|
+
"kind": "object",
|
|
364
|
+
"fields": {
|
|
365
|
+
"action": {
|
|
366
|
+
"kind": "object",
|
|
367
|
+
"fields": {
|
|
368
|
+
"anyOf": {
|
|
369
|
+
"kind": "array",
|
|
370
|
+
"variants": [
|
|
371
|
+
{
|
|
372
|
+
"kind": "object",
|
|
373
|
+
"fields": {
|
|
374
|
+
"type": {
|
|
375
|
+
"kind": "string"
|
|
376
|
+
},
|
|
377
|
+
"const": {
|
|
378
|
+
"kind": "string"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
},
|
|
384
|
+
"description": {
|
|
385
|
+
"kind": "string"
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
"name": {
|
|
390
|
+
"kind": "object",
|
|
391
|
+
"fields": {
|
|
392
|
+
"type": {
|
|
393
|
+
"kind": "string"
|
|
394
|
+
},
|
|
395
|
+
"description": {
|
|
396
|
+
"kind": "string"
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
"file_path": {
|
|
401
|
+
"kind": "object",
|
|
402
|
+
"fields": {
|
|
403
|
+
"type": {
|
|
404
|
+
"kind": "string"
|
|
405
|
+
},
|
|
406
|
+
"description": {
|
|
407
|
+
"kind": "string"
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
"content": {
|
|
412
|
+
"kind": "object",
|
|
413
|
+
"fields": {
|
|
414
|
+
"type": {
|
|
415
|
+
"kind": "string"
|
|
416
|
+
},
|
|
417
|
+
"description": {
|
|
418
|
+
"kind": "string"
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
"old_text": {
|
|
423
|
+
"kind": "object",
|
|
424
|
+
"fields": {
|
|
425
|
+
"type": {
|
|
426
|
+
"kind": "string"
|
|
427
|
+
},
|
|
428
|
+
"description": {
|
|
429
|
+
"kind": "string"
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
"new_text": {
|
|
434
|
+
"kind": "object",
|
|
435
|
+
"fields": {
|
|
436
|
+
"type": {
|
|
437
|
+
"kind": "string"
|
|
438
|
+
},
|
|
439
|
+
"description": {
|
|
440
|
+
"kind": "string"
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
"absorbed_into": {
|
|
445
|
+
"kind": "object",
|
|
446
|
+
"fields": {
|
|
447
|
+
"type": {
|
|
448
|
+
"kind": "string"
|
|
449
|
+
},
|
|
450
|
+
"description": {
|
|
451
|
+
"kind": "string"
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
"reason": {
|
|
456
|
+
"kind": "object",
|
|
457
|
+
"fields": {
|
|
458
|
+
"type": {
|
|
459
|
+
"kind": "string"
|
|
460
|
+
},
|
|
461
|
+
"description": {
|
|
462
|
+
"kind": "string"
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
"overlay_root": {
|
|
467
|
+
"kind": "object",
|
|
468
|
+
"fields": {
|
|
469
|
+
"anyOf": {
|
|
470
|
+
"kind": "array",
|
|
471
|
+
"variants": [
|
|
472
|
+
{
|
|
473
|
+
"kind": "object",
|
|
474
|
+
"fields": {
|
|
475
|
+
"type": {
|
|
476
|
+
"kind": "string"
|
|
477
|
+
},
|
|
478
|
+
"const": {
|
|
479
|
+
"kind": "string"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
]
|
|
484
|
+
},
|
|
485
|
+
"description": {
|
|
486
|
+
"kind": "string"
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
"remove": {
|
|
491
|
+
"kind": "object",
|
|
492
|
+
"fields": {
|
|
493
|
+
"type": {
|
|
494
|
+
"kind": "string"
|
|
495
|
+
},
|
|
496
|
+
"description": {
|
|
497
|
+
"kind": "string"
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
},
|
|
339
507
|
{
|
|
340
508
|
"kind": "object",
|
|
341
509
|
"fields": {
|