@mean-weasel/lineage 0.1.3 → 0.1.4
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 +6 -0
- package/README.md +36 -0
- package/dist/cli/lineage-dev.js +737 -216
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +737 -216
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +599 -62
- package/dist/server.js.map +4 -4
- package/dist/web/assets/index-DI7dn5M6.css +1 -0
- package/dist/web/assets/index-dJy71x2a.js +21 -0
- package/dist/web/index.html +2 -2
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-CTsKYXYF.js +0 -19
- package/dist/web/assets/index-CegGQ4Cv.css +0 -1
package/dist/cli/lineage-dev.js
CHANGED
|
@@ -7,9 +7,17 @@ import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:pa
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
9
|
|
|
10
|
+
// src/server/agentClaims.ts
|
|
11
|
+
import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
// src/server/assetLineageDb.ts
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
16
|
+
import { join as join4 } from "node:path";
|
|
17
|
+
|
|
10
18
|
// src/server/assetCore.ts
|
|
11
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
12
|
-
import { dirname as dirname2, join as
|
|
19
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
13
21
|
import { spawnSync } from "node:child_process";
|
|
14
22
|
import { fileURLToPath } from "node:url";
|
|
15
23
|
|
|
@@ -190,19 +198,244 @@ function createS3StorageAdapter(deps) {
|
|
|
190
198
|
};
|
|
191
199
|
}
|
|
192
200
|
|
|
201
|
+
// src/server/assetCore.ts
|
|
202
|
+
function isPackageRoot(path) {
|
|
203
|
+
const packageJson = join3(path, "package.json");
|
|
204
|
+
if (!existsSync3(packageJson)) return false;
|
|
205
|
+
try {
|
|
206
|
+
const packageInfo = JSON.parse(readFileSync2(packageJson, "utf8"));
|
|
207
|
+
return packageInfo.name === "@mean-weasel/lineage";
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function resolveRepoRoot() {
|
|
213
|
+
const moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
214
|
+
const candidates = [
|
|
215
|
+
process.env.LINEAGE_REPO_ROOT,
|
|
216
|
+
resolve2(moduleDir, ".."),
|
|
217
|
+
resolve2(moduleDir, "../.."),
|
|
218
|
+
process.cwd()
|
|
219
|
+
].filter((candidate) => Boolean(candidate));
|
|
220
|
+
const root = candidates.find(isPackageRoot);
|
|
221
|
+
if (!root) throw new Error("Unable to locate Lineage package root");
|
|
222
|
+
return root;
|
|
223
|
+
}
|
|
224
|
+
var repoRoot = resolveRepoRoot();
|
|
225
|
+
var defaultProject = "demo-project";
|
|
226
|
+
var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
|
|
227
|
+
var publicFallbackBucket = "lineage-demo-assets";
|
|
228
|
+
var publicFallbackRegion = "us-east-1";
|
|
229
|
+
var contentTypes = /* @__PURE__ */ new Set(["image", "video", "gif", "audio", "doc", "other"]);
|
|
230
|
+
var projectNamePattern = /^[a-z0-9][a-z0-9-]*$/;
|
|
231
|
+
var LineageAssetError = class extends Error {
|
|
232
|
+
constructor(message, status = 400) {
|
|
233
|
+
super(message);
|
|
234
|
+
this.status = status;
|
|
235
|
+
}
|
|
236
|
+
status;
|
|
237
|
+
};
|
|
238
|
+
function cleanProject(project = defaultProject) {
|
|
239
|
+
if (!projectNamePattern.test(project)) {
|
|
240
|
+
throw new LineageAssetError("Project must be lowercase kebab-case");
|
|
241
|
+
}
|
|
242
|
+
return project;
|
|
243
|
+
}
|
|
244
|
+
function catalogPath(project = defaultProject) {
|
|
245
|
+
return join3(repoRoot, cleanProject(project), "assets", "catalog.json");
|
|
246
|
+
}
|
|
247
|
+
function fixtureCatalogPath(project = defaultProject) {
|
|
248
|
+
return join3(repoRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
|
|
249
|
+
}
|
|
250
|
+
function normalizeCatalog(catalog, fallbackProject = defaultProject) {
|
|
251
|
+
const project = cleanProject(catalog.project || catalog.product || fallbackProject);
|
|
252
|
+
const product = catalog.product || project;
|
|
253
|
+
return {
|
|
254
|
+
...catalog,
|
|
255
|
+
project,
|
|
256
|
+
product,
|
|
257
|
+
default_bucket: catalog.default_bucket || "",
|
|
258
|
+
default_region: catalog.default_region || "",
|
|
259
|
+
assets: (catalog.assets || []).map((asset) => ({
|
|
260
|
+
...asset,
|
|
261
|
+
source: asset.source || "catalog",
|
|
262
|
+
project: asset.project || asset.product || project,
|
|
263
|
+
product: asset.product || asset.project || project
|
|
264
|
+
}))
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function fallbackS3(assetId, channel, status = "working") {
|
|
268
|
+
return {
|
|
269
|
+
bucket: publicFallbackBucket,
|
|
270
|
+
checksum_sha256: void 0,
|
|
271
|
+
content_type: "image/png",
|
|
272
|
+
key: `products/${defaultProject}/campaigns/2026-06-public-demo/channels/${channel}/audiences/creators/statuses/${status}/types/image/assets/${assetId}/${assetId}.png`,
|
|
273
|
+
region: publicFallbackRegion,
|
|
274
|
+
size_bytes: 2048,
|
|
275
|
+
updated_at: "2026-06-24T12:00:00.000Z",
|
|
276
|
+
version_id: "public-demo-version"
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function fallbackAsset(fields) {
|
|
280
|
+
return {
|
|
281
|
+
audience: fields.audience || "creators",
|
|
282
|
+
campaign: fields.campaign || "2026-06-public-demo",
|
|
283
|
+
content_type: "image",
|
|
284
|
+
cta: fields.cta || "Save the idea",
|
|
285
|
+
hook: fields.hook || "Public demo creative for local review.",
|
|
286
|
+
product: defaultProject,
|
|
287
|
+
project: defaultProject,
|
|
288
|
+
source: "catalog",
|
|
289
|
+
utm_content: fields.utm_content || fields.asset_id.replace(/-/g, "_"),
|
|
290
|
+
...fields
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function defaultFallbackCatalog() {
|
|
294
|
+
return normalizeCatalog({
|
|
295
|
+
assets: [
|
|
296
|
+
fallbackAsset({
|
|
297
|
+
asset_id: "demo-meta-short-form-upload-demo-post-static",
|
|
298
|
+
channel: "meta",
|
|
299
|
+
s3: fallbackS3("demo-meta-short-form-upload-demo-post-static", "meta"),
|
|
300
|
+
status: "working",
|
|
301
|
+
title: "Meta short-form demo post static"
|
|
302
|
+
}),
|
|
303
|
+
fallbackAsset({
|
|
304
|
+
asset_id: "demo-linkedin-ledger-catalog-shared",
|
|
305
|
+
channel: "linkedin",
|
|
306
|
+
hook: "Shared ledger creative with catalog metadata.",
|
|
307
|
+
s3: fallbackS3("demo-linkedin-ledger-catalog-shared", "linkedin"),
|
|
308
|
+
status: "working",
|
|
309
|
+
title: "LinkedIn ledger catalog shared"
|
|
310
|
+
}),
|
|
311
|
+
fallbackAsset({
|
|
312
|
+
asset_id: "demo-linkedin-upload-demo-done-static-grounded-v2",
|
|
313
|
+
channel: "linkedin",
|
|
314
|
+
placements: [{
|
|
315
|
+
channel: "linkedin",
|
|
316
|
+
notes: "Synthetic public scheduled placement.",
|
|
317
|
+
scheduled_at: "2026-06-24T16:00:00-07:00",
|
|
318
|
+
status: "scheduled",
|
|
319
|
+
updated_at: "2026-06-24T12:30:00.000Z"
|
|
320
|
+
}],
|
|
321
|
+
s3: fallbackS3("demo-linkedin-upload-demo-done-static-grounded-v2", "linkedin", "approved"),
|
|
322
|
+
status: "approved",
|
|
323
|
+
title: "LinkedIn upload demo scheduled static"
|
|
324
|
+
}),
|
|
325
|
+
fallbackAsset({
|
|
326
|
+
asset_id: "demo-tiktok-upload-demo-export-vertical",
|
|
327
|
+
channel: "tiktok",
|
|
328
|
+
format: "vertical",
|
|
329
|
+
hook: "Fast vertical demo export for content queue tests.",
|
|
330
|
+
s3: fallbackS3("demo-tiktok-upload-demo-export-vertical", "tiktok"),
|
|
331
|
+
status: "working",
|
|
332
|
+
title: "TikTok upload demo export vertical"
|
|
333
|
+
}),
|
|
334
|
+
fallbackAsset({
|
|
335
|
+
asset_id: "demo-youtube-short-demo-posted-cut",
|
|
336
|
+
channel: "youtube",
|
|
337
|
+
placements: [{
|
|
338
|
+
channel: "youtube",
|
|
339
|
+
notes: "Synthetic public posted placement.",
|
|
340
|
+
posted_at: "2026-06-25T16:00:00-07:00",
|
|
341
|
+
status: "posted",
|
|
342
|
+
updated_at: "2026-06-25T17:00:00.000Z"
|
|
343
|
+
}],
|
|
344
|
+
s3: fallbackS3("demo-youtube-short-demo-posted-cut", "youtube", "published"),
|
|
345
|
+
status: "published",
|
|
346
|
+
title: "YouTube short demo posted cut"
|
|
347
|
+
}),
|
|
348
|
+
fallbackAsset({
|
|
349
|
+
asset_id: "demo-x-twitter-carousel-demo-working-static",
|
|
350
|
+
channel: "x-twitter",
|
|
351
|
+
format: "static",
|
|
352
|
+
s3: fallbackS3("demo-x-twitter-carousel-demo-working-static", "x-twitter"),
|
|
353
|
+
status: "working",
|
|
354
|
+
title: "X Twitter carousel demo working static"
|
|
355
|
+
})
|
|
356
|
+
],
|
|
357
|
+
default_bucket: "",
|
|
358
|
+
default_region: "",
|
|
359
|
+
product: defaultProject,
|
|
360
|
+
project: defaultProject
|
|
361
|
+
}, defaultProject);
|
|
362
|
+
}
|
|
363
|
+
function loadCatalog(project = defaultProject) {
|
|
364
|
+
const path = catalogPath(project);
|
|
365
|
+
if (existsSync3(path)) {
|
|
366
|
+
try {
|
|
367
|
+
return normalizeCatalog(JSON.parse(readFileSync2(path, "utf8")), project);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
370
|
+
throw new LineageAssetError(`Missing catalog: ${path}`, 404);
|
|
371
|
+
}
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const clean = cleanProject(project);
|
|
376
|
+
if (clean === defaultProject) {
|
|
377
|
+
const fixturePath = fixtureCatalogPath(clean);
|
|
378
|
+
if (existsSync3(fixturePath)) {
|
|
379
|
+
return normalizeCatalog(JSON.parse(readFileSync2(fixturePath, "utf8")), project);
|
|
380
|
+
}
|
|
381
|
+
return defaultFallbackCatalog();
|
|
382
|
+
}
|
|
383
|
+
throw new LineageAssetError(`Missing catalog: ${path}`, 404);
|
|
384
|
+
}
|
|
385
|
+
function saveCatalog(project, catalog) {
|
|
386
|
+
const normalized = normalizeCatalog(catalog, project);
|
|
387
|
+
mkdirSync2(dirname2(catalogPath(project)), { recursive: true });
|
|
388
|
+
writeFileSync(catalogPath(project), `${JSON.stringify(normalized, null, 2)}
|
|
389
|
+
`);
|
|
390
|
+
return normalized;
|
|
391
|
+
}
|
|
392
|
+
function run(command, args) {
|
|
393
|
+
const result = spawnSync(command, args, {
|
|
394
|
+
cwd: repoRoot,
|
|
395
|
+
encoding: "utf8",
|
|
396
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
397
|
+
env: {
|
|
398
|
+
...process.env,
|
|
399
|
+
AWS_REGION: process.env.AWS_REGION || "us-east-1",
|
|
400
|
+
AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION || "us-east-1"
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
if (result.status !== 0) {
|
|
404
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
405
|
+
throw new LineageAssetError(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`, 502);
|
|
406
|
+
}
|
|
407
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
408
|
+
}
|
|
409
|
+
function runAws(args) {
|
|
410
|
+
return run("aws", args);
|
|
411
|
+
}
|
|
412
|
+
function assetById(catalog, assetId) {
|
|
413
|
+
const asset = catalog.assets.find((item) => item.asset_id === assetId);
|
|
414
|
+
if (!asset) throw new LineageAssetError(`Unknown asset: ${assetId}`, 404);
|
|
415
|
+
return asset;
|
|
416
|
+
}
|
|
417
|
+
var storageAdapter = createS3StorageAdapter({
|
|
418
|
+
assetById,
|
|
419
|
+
cleanProject,
|
|
420
|
+
createError: (message, status) => new LineageAssetError(message, status),
|
|
421
|
+
defaultProject,
|
|
422
|
+
loadCatalog,
|
|
423
|
+
runAws,
|
|
424
|
+
repoRoot,
|
|
425
|
+
saveCatalog,
|
|
426
|
+
supportedContentTypes: contentTypes
|
|
427
|
+
});
|
|
428
|
+
|
|
193
429
|
// src/server/assetLineageDb.ts
|
|
194
|
-
import { createRequire } from "node:module";
|
|
195
|
-
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
196
|
-
import { join as join3 } from "node:path";
|
|
197
430
|
var require2 = createRequire(import.meta.url);
|
|
198
431
|
function nowIso() {
|
|
199
432
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
200
433
|
}
|
|
201
434
|
function lineageDbPath() {
|
|
202
|
-
return process.env.LINEAGE_DB ||
|
|
435
|
+
return process.env.LINEAGE_DB || join4(repoRoot, ".lineage", "asset-lineage.sqlite");
|
|
203
436
|
}
|
|
204
437
|
function lineageDb() {
|
|
205
|
-
|
|
438
|
+
mkdirSync3(join4(lineageDbPath(), ".."), { recursive: true });
|
|
206
439
|
const { DatabaseSync } = require2("node:sqlite");
|
|
207
440
|
const database = new DatabaseSync(lineageDbPath());
|
|
208
441
|
database.exec("PRAGMA foreign_keys = ON");
|
|
@@ -504,6 +737,41 @@ function lineageDb() {
|
|
|
504
737
|
);
|
|
505
738
|
create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
|
|
506
739
|
create table if not exists adapter_settings (project_id text not null references projects(id), adapter_type text not null check (adapter_type in ('cloud', 'scheduler', 'image_generator')), provider text not null, enabled integer not null check (enabled in (0, 1)), secret_ref text, safe_config_json text not null, created_at text not null, updated_at text not null, primary key(project_id, adapter_type, provider)); create index if not exists adapter_settings_project_type on adapter_settings(project_id, adapter_type);
|
|
740
|
+
create table if not exists agent_claims (
|
|
741
|
+
id text primary key,
|
|
742
|
+
token_hash text not null,
|
|
743
|
+
project_id text not null references projects(id),
|
|
744
|
+
channel text,
|
|
745
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
746
|
+
target_id text not null,
|
|
747
|
+
target_title text,
|
|
748
|
+
agent_id text,
|
|
749
|
+
agent_name text not null,
|
|
750
|
+
agent_kind text not null,
|
|
751
|
+
thread_id text,
|
|
752
|
+
status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
|
|
753
|
+
created_at text not null,
|
|
754
|
+
heartbeat_at text not null,
|
|
755
|
+
expires_at text not null,
|
|
756
|
+
released_at text,
|
|
757
|
+
revoked_at text,
|
|
758
|
+
revoked_by text,
|
|
759
|
+
override_reason text,
|
|
760
|
+
metadata_json text
|
|
761
|
+
);
|
|
762
|
+
create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
|
|
763
|
+
create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
|
|
764
|
+
create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
|
|
765
|
+
create table if not exists agent_claim_events (
|
|
766
|
+
id text primary key,
|
|
767
|
+
claim_id text not null references agent_claims(id) on delete cascade,
|
|
768
|
+
event_type text not null,
|
|
769
|
+
actor text,
|
|
770
|
+
message text,
|
|
771
|
+
created_at text not null,
|
|
772
|
+
metadata_json text
|
|
773
|
+
);
|
|
774
|
+
create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
|
|
507
775
|
`);
|
|
508
776
|
migrateAssetSelections(database);
|
|
509
777
|
dropLegacyAssetSelectionRootUnique(database);
|
|
@@ -579,233 +847,362 @@ function ensureReviewStateValues(database) {
|
|
|
579
847
|
`);
|
|
580
848
|
}
|
|
581
849
|
|
|
582
|
-
// src/server/
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return false;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
function resolveRepoRoot() {
|
|
594
|
-
const moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
595
|
-
const candidates = [
|
|
596
|
-
process.env.LINEAGE_REPO_ROOT,
|
|
597
|
-
resolve2(moduleDir, ".."),
|
|
598
|
-
resolve2(moduleDir, "../.."),
|
|
599
|
-
process.cwd()
|
|
600
|
-
].filter((candidate) => Boolean(candidate));
|
|
601
|
-
const root = candidates.find(isPackageRoot);
|
|
602
|
-
if (!root) throw new Error("Unable to locate Lineage package root");
|
|
603
|
-
return root;
|
|
604
|
-
}
|
|
605
|
-
var repoRoot = resolveRepoRoot();
|
|
606
|
-
var defaultProject = "demo-project";
|
|
607
|
-
var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
|
|
608
|
-
var publicFallbackBucket = "lineage-demo-assets";
|
|
609
|
-
var publicFallbackRegion = "us-east-1";
|
|
610
|
-
var contentTypes = /* @__PURE__ */ new Set(["image", "video", "gif", "audio", "doc", "other"]);
|
|
611
|
-
var projectNamePattern = /^[a-z0-9][a-z0-9-]*$/;
|
|
612
|
-
var LineageAssetError = class extends Error {
|
|
613
|
-
constructor(message, status = 400) {
|
|
850
|
+
// src/server/agentClaims.ts
|
|
851
|
+
var defaultTtlSeconds = 20 * 60;
|
|
852
|
+
var idleAfterSeconds = 5 * 60;
|
|
853
|
+
var staleAfterSeconds = 15 * 60;
|
|
854
|
+
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
855
|
+
var claimTokenPattern = /claim_[a-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
856
|
+
var AgentClaimError = class extends Error {
|
|
857
|
+
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
614
858
|
super(message);
|
|
615
859
|
this.status = status;
|
|
860
|
+
this.code = code;
|
|
861
|
+
this.conflicts = conflicts;
|
|
616
862
|
}
|
|
617
863
|
status;
|
|
864
|
+
code;
|
|
865
|
+
conflicts;
|
|
618
866
|
};
|
|
619
|
-
function
|
|
620
|
-
|
|
621
|
-
|
|
867
|
+
function isAgentClaimError(error) {
|
|
868
|
+
return error instanceof AgentClaimError;
|
|
869
|
+
}
|
|
870
|
+
function redactAgentClaimTokens(input) {
|
|
871
|
+
return input.replace(claimTokenPattern, "[redacted-claim-token]");
|
|
872
|
+
}
|
|
873
|
+
function parseClaimTtl(value) {
|
|
874
|
+
if (!value) return defaultTtlSeconds;
|
|
875
|
+
const match = value.trim().match(/^(\d+)(s|m|h)?$/);
|
|
876
|
+
if (!match) throw new AgentClaimError(`Invalid claim ttl: ${value}`);
|
|
877
|
+
const amount = Number(match[1]);
|
|
878
|
+
const unit = match[2] || "s";
|
|
879
|
+
const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
|
|
880
|
+
const seconds = amount * multiplier;
|
|
881
|
+
if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
|
|
882
|
+
throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
|
|
622
883
|
}
|
|
623
|
-
return
|
|
624
|
-
}
|
|
625
|
-
function catalogPath(project = defaultProject) {
|
|
626
|
-
return join4(repoRoot, cleanProject(project), "assets", "catalog.json");
|
|
884
|
+
return seconds;
|
|
627
885
|
}
|
|
628
|
-
function
|
|
629
|
-
return
|
|
630
|
-
}
|
|
631
|
-
function normalizeCatalog(catalog, fallbackProject = defaultProject) {
|
|
632
|
-
const project = cleanProject(catalog.project || catalog.product || fallbackProject);
|
|
633
|
-
const product = catalog.product || project;
|
|
634
|
-
return {
|
|
635
|
-
...catalog,
|
|
636
|
-
project,
|
|
637
|
-
product,
|
|
638
|
-
default_bucket: catalog.default_bucket || "",
|
|
639
|
-
default_region: catalog.default_region || "",
|
|
640
|
-
assets: (catalog.assets || []).map((asset) => ({
|
|
641
|
-
...asset,
|
|
642
|
-
source: asset.source || "catalog",
|
|
643
|
-
project: asset.project || asset.product || project,
|
|
644
|
-
product: asset.product || asset.project || project
|
|
645
|
-
}))
|
|
646
|
-
};
|
|
886
|
+
function randomId(prefix) {
|
|
887
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
647
888
|
}
|
|
648
|
-
function
|
|
889
|
+
function tokenHash(token) {
|
|
890
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
891
|
+
}
|
|
892
|
+
function safeEqual(a, b) {
|
|
893
|
+
const left = Buffer.from(a);
|
|
894
|
+
const right = Buffer.from(b);
|
|
895
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
896
|
+
}
|
|
897
|
+
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
898
|
+
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
899
|
+
}
|
|
900
|
+
function metadataJson(metadata) {
|
|
901
|
+
return metadata ? JSON.stringify(metadata) : null;
|
|
902
|
+
}
|
|
903
|
+
function parseMetadata(value) {
|
|
904
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
905
|
+
try {
|
|
906
|
+
const parsed = JSON.parse(value);
|
|
907
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
908
|
+
} catch {
|
|
909
|
+
return void 0;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
913
|
+
if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
|
|
914
|
+
if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
|
|
915
|
+
const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
|
|
916
|
+
if (ageSeconds >= staleAfterSeconds) return "stale";
|
|
917
|
+
if (ageSeconds >= idleAfterSeconds) return "idle";
|
|
918
|
+
return "active";
|
|
919
|
+
}
|
|
920
|
+
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
921
|
+
const heartbeatAt = String(row.heartbeat_at);
|
|
649
922
|
return {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
923
|
+
id: String(row.id),
|
|
924
|
+
project: String(row.project_id),
|
|
925
|
+
channel: typeof row.channel === "string" ? row.channel : void 0,
|
|
926
|
+
scope_type: String(row.scope_type),
|
|
927
|
+
target_id: String(row.target_id),
|
|
928
|
+
target_title: typeof row.target_title === "string" ? row.target_title : void 0,
|
|
929
|
+
agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
|
|
930
|
+
agent_name: String(row.agent_name),
|
|
931
|
+
agent_kind: String(row.agent_kind),
|
|
932
|
+
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
933
|
+
status: String(row.status),
|
|
934
|
+
created_at: String(row.created_at),
|
|
935
|
+
heartbeat_at: heartbeatAt,
|
|
936
|
+
expires_at: String(row.expires_at),
|
|
937
|
+
released_at: typeof row.released_at === "string" ? row.released_at : void 0,
|
|
938
|
+
revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
|
|
939
|
+
revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
|
|
940
|
+
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
941
|
+
metadata: parseMetadata(row.metadata_json),
|
|
942
|
+
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
943
|
+
derived_state: derivedState({
|
|
944
|
+
expires_at: String(row.expires_at),
|
|
945
|
+
heartbeat_at: heartbeatAt,
|
|
946
|
+
status: String(row.status)
|
|
947
|
+
}, now)
|
|
658
948
|
};
|
|
659
949
|
}
|
|
660
|
-
function
|
|
950
|
+
function eventToRow(row) {
|
|
661
951
|
return {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
project: defaultProject,
|
|
669
|
-
source: "catalog",
|
|
670
|
-
utm_content: fields.utm_content || fields.asset_id.replace(/-/g, "_"),
|
|
671
|
-
...fields
|
|
952
|
+
claim_id: String(row.claim_id),
|
|
953
|
+
event_type: String(row.event_type),
|
|
954
|
+
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
955
|
+
message: typeof row.message === "string" ? row.message : void 0,
|
|
956
|
+
created_at: String(row.created_at),
|
|
957
|
+
metadata: parseMetadata(row.metadata_json)
|
|
672
958
|
};
|
|
673
959
|
}
|
|
674
|
-
function
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
status: "working",
|
|
682
|
-
title: "Meta short-form demo post static"
|
|
683
|
-
}),
|
|
684
|
-
fallbackAsset({
|
|
685
|
-
asset_id: "demo-linkedin-ledger-catalog-shared",
|
|
686
|
-
channel: "linkedin",
|
|
687
|
-
hook: "Shared ledger creative with catalog metadata.",
|
|
688
|
-
s3: fallbackS3("demo-linkedin-ledger-catalog-shared", "linkedin"),
|
|
689
|
-
status: "working",
|
|
690
|
-
title: "LinkedIn ledger catalog shared"
|
|
691
|
-
}),
|
|
692
|
-
fallbackAsset({
|
|
693
|
-
asset_id: "demo-linkedin-upload-demo-done-static-grounded-v2",
|
|
694
|
-
channel: "linkedin",
|
|
695
|
-
placements: [{
|
|
696
|
-
channel: "linkedin",
|
|
697
|
-
notes: "Synthetic public scheduled placement.",
|
|
698
|
-
scheduled_at: "2026-06-24T16:00:00-07:00",
|
|
699
|
-
status: "scheduled",
|
|
700
|
-
updated_at: "2026-06-24T12:30:00.000Z"
|
|
701
|
-
}],
|
|
702
|
-
s3: fallbackS3("demo-linkedin-upload-demo-done-static-grounded-v2", "linkedin", "approved"),
|
|
703
|
-
status: "approved",
|
|
704
|
-
title: "LinkedIn upload demo scheduled static"
|
|
705
|
-
}),
|
|
706
|
-
fallbackAsset({
|
|
707
|
-
asset_id: "demo-tiktok-upload-demo-export-vertical",
|
|
708
|
-
channel: "tiktok",
|
|
709
|
-
format: "vertical",
|
|
710
|
-
hook: "Fast vertical demo export for content queue tests.",
|
|
711
|
-
s3: fallbackS3("demo-tiktok-upload-demo-export-vertical", "tiktok"),
|
|
712
|
-
status: "working",
|
|
713
|
-
title: "TikTok upload demo export vertical"
|
|
714
|
-
}),
|
|
715
|
-
fallbackAsset({
|
|
716
|
-
asset_id: "demo-youtube-short-demo-posted-cut",
|
|
717
|
-
channel: "youtube",
|
|
718
|
-
placements: [{
|
|
719
|
-
channel: "youtube",
|
|
720
|
-
notes: "Synthetic public posted placement.",
|
|
721
|
-
posted_at: "2026-06-25T16:00:00-07:00",
|
|
722
|
-
status: "posted",
|
|
723
|
-
updated_at: "2026-06-25T17:00:00.000Z"
|
|
724
|
-
}],
|
|
725
|
-
s3: fallbackS3("demo-youtube-short-demo-posted-cut", "youtube", "published"),
|
|
726
|
-
status: "published",
|
|
727
|
-
title: "YouTube short demo posted cut"
|
|
728
|
-
}),
|
|
729
|
-
fallbackAsset({
|
|
730
|
-
asset_id: "demo-x-twitter-carousel-demo-working-static",
|
|
731
|
-
channel: "x-twitter",
|
|
732
|
-
format: "static",
|
|
733
|
-
s3: fallbackS3("demo-x-twitter-carousel-demo-working-static", "x-twitter"),
|
|
734
|
-
status: "working",
|
|
735
|
-
title: "X Twitter carousel demo working static"
|
|
736
|
-
})
|
|
737
|
-
],
|
|
738
|
-
default_bucket: "",
|
|
739
|
-
default_region: "",
|
|
740
|
-
product: defaultProject,
|
|
741
|
-
project: defaultProject
|
|
742
|
-
}, defaultProject);
|
|
960
|
+
function ensureProject(database, project) {
|
|
961
|
+
const timestamp = nowIso();
|
|
962
|
+
database.prepare(`
|
|
963
|
+
insert into projects (id, product, created_at, updated_at)
|
|
964
|
+
values (?, ?, ?, ?)
|
|
965
|
+
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
966
|
+
`).run(project, project, timestamp, timestamp);
|
|
743
967
|
}
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
968
|
+
function recordEvent(database, claimId, eventType, actor, message, metadata) {
|
|
969
|
+
database.prepare(`
|
|
970
|
+
insert into agent_claim_events (id, claim_id, event_type, actor, message, created_at, metadata_json)
|
|
971
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
972
|
+
`).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata));
|
|
973
|
+
}
|
|
974
|
+
function expireActiveClaims(database) {
|
|
975
|
+
const timestamp = nowIso();
|
|
976
|
+
const expired = database.prepare(`
|
|
977
|
+
select id from agent_claims where status = 'active' and expires_at <= ?
|
|
978
|
+
`).all(timestamp);
|
|
979
|
+
if (expired.length === 0) return;
|
|
980
|
+
database.prepare(`
|
|
981
|
+
update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
|
|
982
|
+
`).run(timestamp);
|
|
983
|
+
for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
|
|
984
|
+
}
|
|
985
|
+
function normalizeScope(scopeType) {
|
|
986
|
+
if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
|
|
987
|
+
return scopeType;
|
|
988
|
+
}
|
|
989
|
+
function channelOverlaps(left, right) {
|
|
990
|
+
return !left || !right || left === right;
|
|
991
|
+
}
|
|
992
|
+
function claimOverlaps(candidate, existing) {
|
|
993
|
+
if (candidate.project !== existing.project) return false;
|
|
994
|
+
if (!channelOverlaps(candidate.channel, existing.channel)) return false;
|
|
995
|
+
if (candidate.scope_type === existing.scope_type && candidate.target_id === existing.target_id) return true;
|
|
996
|
+
return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
|
|
997
|
+
}
|
|
998
|
+
function activeClaims(database, project) {
|
|
999
|
+
const rows = project ? database.prepare("select * from agent_claims where project_id = ? and status = 'active' order by heartbeat_at desc").all(project) : database.prepare("select * from agent_claims where status = 'active' order by project_id, heartbeat_at desc").all();
|
|
1000
|
+
return rows.map((row) => rowToClaim(row));
|
|
1001
|
+
}
|
|
1002
|
+
function findClaimById(database, claimId, project) {
|
|
1003
|
+
const row = project ? database.prepare("select * from agent_claims where project_id = ? and id = ?").get(project, claimId) : database.prepare("select * from agent_claims where id = ?").get(claimId);
|
|
1004
|
+
return row ? rowToClaim(row) : null;
|
|
1005
|
+
}
|
|
1006
|
+
function findClaimRowByToken(database, claimToken) {
|
|
1007
|
+
const claimId = claimToken.split(".")[0];
|
|
1008
|
+
const row = database.prepare("select * from agent_claims where id = ?").get(claimId);
|
|
1009
|
+
if (!row) return null;
|
|
1010
|
+
return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
|
|
1011
|
+
}
|
|
1012
|
+
function denied(code, message, conflicts = []) {
|
|
1013
|
+
return { ok: false, code, message, conflicts };
|
|
1014
|
+
}
|
|
1015
|
+
function scopeAllowsWrite(claim, scopeType, targetId, writeKind) {
|
|
1016
|
+
if (claim.scope_type === scopeType && claim.target_id === targetId) return true;
|
|
1017
|
+
if (claim.scope_type === "project_channel") return true;
|
|
1018
|
+
if (claim.scope_type === "lineage_workspace" && scopeType === "lineage_workspace") return claim.target_id === targetId;
|
|
1019
|
+
if (claim.scope_type === "content_queue_lane" && writeKind === "content_queue_next") return true;
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
function createAgentClaim(fields) {
|
|
1023
|
+
const project = fields.project.trim();
|
|
1024
|
+
const targetId = fields.targetId.trim();
|
|
1025
|
+
const agentName = fields.agentName.trim();
|
|
1026
|
+
if (!project) throw new AgentClaimError("Agent claim requires project");
|
|
1027
|
+
if (!targetId) throw new AgentClaimError("Agent claim requires target");
|
|
1028
|
+
if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
|
|
1029
|
+
const scopeType = normalizeScope(fields.scopeType);
|
|
1030
|
+
const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
|
|
1031
|
+
const database = lineageDb();
|
|
1032
|
+
try {
|
|
1033
|
+
ensureProject(database, project);
|
|
1034
|
+
expireActiveClaims(database);
|
|
1035
|
+
const candidate = { project, channel: fields.channel?.trim() || void 0, scope_type: scopeType, target_id: targetId };
|
|
1036
|
+
const conflicts = activeClaims(database, project).filter((claim2) => claimOverlaps(candidate, claim2));
|
|
1037
|
+
if (conflicts.length > 0 && !fields.force) {
|
|
1038
|
+
throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
|
|
754
1039
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (clean === defaultProject) {
|
|
758
|
-
const fixturePath = fixtureCatalogPath(clean);
|
|
759
|
-
if (existsSync3(fixturePath)) {
|
|
760
|
-
return normalizeCatalog(JSON.parse(readFileSync2(fixturePath, "utf8")), project);
|
|
1040
|
+
if (conflicts.length > 0 && !fields.reason?.trim()) {
|
|
1041
|
+
throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
|
|
761
1042
|
}
|
|
762
|
-
|
|
1043
|
+
const timestamp = nowIso();
|
|
1044
|
+
for (const conflict of conflicts) {
|
|
1045
|
+
database.prepare(`
|
|
1046
|
+
update agent_claims
|
|
1047
|
+
set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1048
|
+
where id = ? and status = 'active'
|
|
1049
|
+
`).run(timestamp, agentName, fields.reason || null, conflict.id);
|
|
1050
|
+
recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
|
|
1051
|
+
recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
|
|
1052
|
+
}
|
|
1053
|
+
const id = randomId("claim");
|
|
1054
|
+
const secret = randomBytes(24).toString("base64url");
|
|
1055
|
+
const claimToken = `${id}.${secret}`;
|
|
1056
|
+
const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
|
|
1057
|
+
database.prepare(`
|
|
1058
|
+
insert into agent_claims (
|
|
1059
|
+
id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
|
|
1060
|
+
agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
|
|
1061
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
|
|
1062
|
+
`).run(
|
|
1063
|
+
id,
|
|
1064
|
+
tokenHash(claimToken),
|
|
1065
|
+
project,
|
|
1066
|
+
candidate.channel || null,
|
|
1067
|
+
scopeType,
|
|
1068
|
+
targetId,
|
|
1069
|
+
fields.targetTitle?.trim() || null,
|
|
1070
|
+
fields.agentId?.trim() || null,
|
|
1071
|
+
agentName,
|
|
1072
|
+
fields.agentKind?.trim() || "codex",
|
|
1073
|
+
fields.threadId?.trim() || null,
|
|
1074
|
+
timestamp,
|
|
1075
|
+
timestamp,
|
|
1076
|
+
expiresAt,
|
|
1077
|
+
metadataJson(fields.metadata)
|
|
1078
|
+
);
|
|
1079
|
+
recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
|
|
1080
|
+
const claim = findClaimById(database, id);
|
|
1081
|
+
return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
|
|
1082
|
+
} finally {
|
|
1083
|
+
database.close();
|
|
763
1084
|
}
|
|
764
|
-
throw new LineageAssetError(`Missing catalog: ${path}`, 404);
|
|
765
1085
|
}
|
|
766
|
-
function
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1086
|
+
function listAgentClaims(project) {
|
|
1087
|
+
const database = lineageDb();
|
|
1088
|
+
try {
|
|
1089
|
+
expireActiveClaims(database);
|
|
1090
|
+
const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
|
|
1091
|
+
return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
|
|
1092
|
+
} finally {
|
|
1093
|
+
database.close();
|
|
1094
|
+
}
|
|
772
1095
|
}
|
|
773
|
-
function
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
});
|
|
784
|
-
if (result.status !== 0) {
|
|
785
|
-
const detail = result.stderr.trim() || result.stdout.trim();
|
|
786
|
-
throw new LineageAssetError(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`, 502);
|
|
1096
|
+
function inspectAgentClaim(claimId, project) {
|
|
1097
|
+
const database = lineageDb();
|
|
1098
|
+
try {
|
|
1099
|
+
expireActiveClaims(database);
|
|
1100
|
+
const claim = findClaimById(database, claimId, project);
|
|
1101
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1102
|
+
const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
|
|
1103
|
+
return { ok: true, claim, events: events.map(eventToRow) };
|
|
1104
|
+
} finally {
|
|
1105
|
+
database.close();
|
|
787
1106
|
}
|
|
788
|
-
return { stdout: result.stdout, stderr: result.stderr };
|
|
789
1107
|
}
|
|
790
|
-
function
|
|
791
|
-
|
|
1108
|
+
function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
|
|
1109
|
+
const database = lineageDb();
|
|
1110
|
+
try {
|
|
1111
|
+
expireActiveClaims(database);
|
|
1112
|
+
const row = findClaimRowByToken(database, claimToken);
|
|
1113
|
+
if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
|
|
1114
|
+
const claim = rowToClaim(row);
|
|
1115
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1116
|
+
const timestamp = nowIso();
|
|
1117
|
+
database.prepare("update agent_claims set heartbeat_at = ?, expires_at = ? where id = ?").run(timestamp, expiresAtFrom(timestamp, ttlSeconds), claim.id);
|
|
1118
|
+
recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
|
|
1119
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1120
|
+
} finally {
|
|
1121
|
+
database.close();
|
|
1122
|
+
}
|
|
792
1123
|
}
|
|
793
|
-
function
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
1124
|
+
function releaseAgentClaim(claimToken) {
|
|
1125
|
+
const database = lineageDb();
|
|
1126
|
+
try {
|
|
1127
|
+
expireActiveClaims(database);
|
|
1128
|
+
const row = findClaimRowByToken(database, claimToken);
|
|
1129
|
+
if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
|
|
1130
|
+
const claim = rowToClaim(row);
|
|
1131
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1132
|
+
const timestamp = nowIso();
|
|
1133
|
+
database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
|
|
1134
|
+
recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
|
|
1135
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1136
|
+
} finally {
|
|
1137
|
+
database.close();
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function revokeAgentClaim(project, claimId, fields) {
|
|
1141
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1142
|
+
if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
|
|
1143
|
+
const database = lineageDb();
|
|
1144
|
+
try {
|
|
1145
|
+
expireActiveClaims(database);
|
|
1146
|
+
const claim = findClaimById(database, claimId, project);
|
|
1147
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1148
|
+
const timestamp = nowIso();
|
|
1149
|
+
database.prepare(`
|
|
1150
|
+
update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1151
|
+
where id = ?
|
|
1152
|
+
`).run(timestamp, fields.actor || "human", fields.reason, claim.id);
|
|
1153
|
+
recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
|
|
1154
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1155
|
+
} finally {
|
|
1156
|
+
database.close();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function transferAgentClaim(project, claimId, fields) {
|
|
1160
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1161
|
+
const toAgentName = fields.toAgentName.trim();
|
|
1162
|
+
if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
|
|
1163
|
+
const database = lineageDb();
|
|
1164
|
+
try {
|
|
1165
|
+
expireActiveClaims(database);
|
|
1166
|
+
const claim = findClaimById(database, claimId, project);
|
|
1167
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1168
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1169
|
+
database.prepare("update agent_claims set agent_name = ? where id = ?").run(toAgentName, claim.id);
|
|
1170
|
+
recordEvent(database, claim.id, "transferred", fields.actor || "human", fields.reason || `Transferred claim to ${toAgentName}.`, { to_agent_name: toAgentName });
|
|
1171
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1172
|
+
} finally {
|
|
1173
|
+
database.close();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function validateAgentClaimForWrite(fields) {
|
|
1177
|
+
if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
|
|
1178
|
+
return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
|
|
1179
|
+
}
|
|
1180
|
+
if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
|
|
1181
|
+
const database = lineageDb();
|
|
1182
|
+
try {
|
|
1183
|
+
expireActiveClaims(database);
|
|
1184
|
+
const row = findClaimRowByToken(database, fields.claimToken);
|
|
1185
|
+
if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
|
|
1186
|
+
const claim = rowToClaim(row);
|
|
1187
|
+
if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
|
|
1188
|
+
if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
|
|
1189
|
+
if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
|
|
1190
|
+
if (fields.channel && claim.channel && claim.channel !== fields.channel) {
|
|
1191
|
+
return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
|
|
1192
|
+
}
|
|
1193
|
+
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
1194
|
+
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
1195
|
+
}
|
|
1196
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1197
|
+
danger_level: fields.dangerLevel,
|
|
1198
|
+
target_id: fields.targetId,
|
|
1199
|
+
write_kind: fields.writeKind
|
|
1200
|
+
});
|
|
1201
|
+
return { ok: true, claim, warnings: [] };
|
|
1202
|
+
} finally {
|
|
1203
|
+
database.close();
|
|
1204
|
+
}
|
|
797
1205
|
}
|
|
798
|
-
var storageAdapter = createS3StorageAdapter({
|
|
799
|
-
assetById,
|
|
800
|
-
cleanProject,
|
|
801
|
-
createError: (message, status) => new LineageAssetError(message, status),
|
|
802
|
-
defaultProject,
|
|
803
|
-
loadCatalog,
|
|
804
|
-
runAws,
|
|
805
|
-
repoRoot,
|
|
806
|
-
saveCatalog,
|
|
807
|
-
supportedContentTypes: contentTypes
|
|
808
|
-
});
|
|
809
1206
|
|
|
810
1207
|
// src/server/assetLineageSelection.ts
|
|
811
1208
|
function selectedRows(database, project, root) {
|
|
@@ -821,7 +1218,7 @@ function selectedRows(database, project, root) {
|
|
|
821
1218
|
function lineageWorkspaceId(project, rootAssetId) {
|
|
822
1219
|
return `${project}:lineage-workspace:${rootAssetId}`;
|
|
823
1220
|
}
|
|
824
|
-
function
|
|
1221
|
+
function ensureProject2(database, project) {
|
|
825
1222
|
const timestamp = nowIso();
|
|
826
1223
|
database.prepare(`
|
|
827
1224
|
insert into projects (id, product, created_at, updated_at)
|
|
@@ -858,7 +1255,7 @@ function knownRoots(database, project) {
|
|
|
858
1255
|
return rows.map((row) => ({ root_asset_id: row.root_asset_id, selected_at: row.selected_at || void 0 }));
|
|
859
1256
|
}
|
|
860
1257
|
function seedLegacyWorkspaces(database, project) {
|
|
861
|
-
|
|
1258
|
+
ensureProject2(database, project);
|
|
862
1259
|
const timestamp = nowIso();
|
|
863
1260
|
const statement = database.prepare(`
|
|
864
1261
|
insert into lineage_workspaces (
|
|
@@ -1168,9 +1565,23 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
1168
1565
|
function linkSelectedLineageChild(project, fields) {
|
|
1169
1566
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
1170
1567
|
if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
|
|
1568
|
+
if (fields.confirmWrite) {
|
|
1569
|
+
const validation = validateAgentClaimForWrite({
|
|
1570
|
+
channel: next.next_asset.channel,
|
|
1571
|
+
claimToken: fields.claimToken,
|
|
1572
|
+
confirmWrite: fields.confirmWrite,
|
|
1573
|
+
dangerLevel: "enforce",
|
|
1574
|
+
project,
|
|
1575
|
+
scopeType: "lineage_workspace",
|
|
1576
|
+
targetId: lineageWorkspaceId(project, next.root_asset_id),
|
|
1577
|
+
writeKind: "link_child"
|
|
1578
|
+
});
|
|
1579
|
+
if (!validation.ok) throw new AgentClaimError(validation.message, validation.code === "claim_required" ? 401 : 403, validation.code, validation.conflicts);
|
|
1580
|
+
}
|
|
1171
1581
|
const result = linkLineageAssets(project, {
|
|
1172
1582
|
childAssetId: fields.childAssetId,
|
|
1173
1583
|
confirmWrite: fields.confirmWrite,
|
|
1584
|
+
claimToken: fields.claimToken,
|
|
1174
1585
|
parentAssetId: next.next_asset.asset_id
|
|
1175
1586
|
});
|
|
1176
1587
|
return {
|
|
@@ -1236,7 +1647,14 @@ Usage:
|
|
|
1236
1647
|
${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
1237
1648
|
${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
|
|
1238
1649
|
${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
|
|
1239
|
-
${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
1650
|
+
${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
|
|
1651
|
+
${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
|
|
1652
|
+
${config.binName} agent status [--project <project>] [--json]
|
|
1653
|
+
${config.binName} agent inspect --claim <claim-id> [--project <project>] [--json]
|
|
1654
|
+
${config.binName} agent heartbeat --claim-token <claim-id.secret> [--json]
|
|
1655
|
+
${config.binName} agent release --claim-token <claim-id.secret> [--json]
|
|
1656
|
+
${config.binName} agent revoke --claim <claim-id> --project <project> --reason <text> --confirm-write [--json]
|
|
1657
|
+
${config.binName} agent transfer --claim <claim-id> --to-agent-name <name> --confirm-write [--project <project>] [--json]
|
|
1240
1658
|
${config.binName} --help
|
|
1241
1659
|
${config.binName} --version
|
|
1242
1660
|
|
|
@@ -1265,6 +1683,7 @@ function resolveDataCommandOptions(args) {
|
|
|
1265
1683
|
const options = {
|
|
1266
1684
|
assetId: readOption(args, "--asset-id") || positions[0],
|
|
1267
1685
|
childAssetId: readOption(args, "--child"),
|
|
1686
|
+
claimToken: readOption(args, "--claim-token") || process.env.LINEAGE_CLAIM_TOKEN,
|
|
1268
1687
|
confirmWrite: args.includes("--confirm-write"),
|
|
1269
1688
|
dbPath: readOption(args, "--db"),
|
|
1270
1689
|
json: args.includes("--json"),
|
|
@@ -1286,6 +1705,7 @@ function runLineageDataCommand(command, args) {
|
|
|
1286
1705
|
if (!options.childAssetId) throw new Error("lineage link-child requires --child");
|
|
1287
1706
|
return linkSelectedLineageChild(options.project, {
|
|
1288
1707
|
childAssetId: options.childAssetId,
|
|
1708
|
+
claimToken: options.claimToken,
|
|
1289
1709
|
confirmWrite: options.confirmWrite,
|
|
1290
1710
|
rootAssetId: options.rootAssetId || options.assetId
|
|
1291
1711
|
});
|
|
@@ -1322,6 +1742,87 @@ function printDataResult(command, result, json) {
|
|
|
1322
1742
|
}
|
|
1323
1743
|
console.log(String(result));
|
|
1324
1744
|
}
|
|
1745
|
+
function runLineageAgentCommand(command, args) {
|
|
1746
|
+
const dbPath = readOption(args, "--db");
|
|
1747
|
+
if (dbPath) process.env.LINEAGE_DB = dbPath;
|
|
1748
|
+
const project = readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct;
|
|
1749
|
+
const claimId = readOption(args, "--claim");
|
|
1750
|
+
const claimToken = readOption(args, "--claim-token") || process.env.LINEAGE_CLAIM_TOKEN;
|
|
1751
|
+
if (command === "claim") {
|
|
1752
|
+
return createAgentClaim({
|
|
1753
|
+
agentId: readOption(args, "--agent-id"),
|
|
1754
|
+
agentKind: readOption(args, "--agent-kind"),
|
|
1755
|
+
agentName: readOption(args, "--agent-name") || "",
|
|
1756
|
+
channel: readOption(args, "--channel"),
|
|
1757
|
+
force: args.includes("--force"),
|
|
1758
|
+
project,
|
|
1759
|
+
reason: readOption(args, "--reason"),
|
|
1760
|
+
scopeType: readOption(args, "--scope") || "",
|
|
1761
|
+
targetId: readOption(args, "--target") || "",
|
|
1762
|
+
targetTitle: readOption(args, "--target-title"),
|
|
1763
|
+
threadId: readOption(args, "--thread-id"),
|
|
1764
|
+
ttlSeconds: parseClaimTtl(readOption(args, "--ttl"))
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
if (command === "status") return listAgentClaims(project);
|
|
1768
|
+
if (command === "inspect") {
|
|
1769
|
+
if (!claimId) throw new Error("lineage agent inspect requires --claim");
|
|
1770
|
+
return inspectAgentClaim(claimId, project);
|
|
1771
|
+
}
|
|
1772
|
+
if (command === "heartbeat") {
|
|
1773
|
+
if (!claimToken) throw new Error("lineage agent heartbeat requires --claim-token");
|
|
1774
|
+
return heartbeatAgentClaim(claimToken, parseClaimTtl(readOption(args, "--ttl")));
|
|
1775
|
+
}
|
|
1776
|
+
if (command === "release") {
|
|
1777
|
+
if (!claimToken) throw new Error("lineage agent release requires --claim-token");
|
|
1778
|
+
return releaseAgentClaim(claimToken);
|
|
1779
|
+
}
|
|
1780
|
+
if (command === "revoke") {
|
|
1781
|
+
if (!claimId) throw new Error("lineage agent revoke requires --claim");
|
|
1782
|
+
return revokeAgentClaim(project, claimId, {
|
|
1783
|
+
actor: readOption(args, "--actor") || "human",
|
|
1784
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
1785
|
+
reason: readOption(args, "--reason")
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
if (command === "transfer") {
|
|
1789
|
+
if (!claimId) throw new Error("lineage agent transfer requires --claim");
|
|
1790
|
+
return transferAgentClaim(project, claimId, {
|
|
1791
|
+
actor: readOption(args, "--actor") || "human",
|
|
1792
|
+
confirmWrite: args.includes("--confirm-write"),
|
|
1793
|
+
reason: readOption(args, "--reason"),
|
|
1794
|
+
toAgentName: readOption(args, "--to-agent-name") || ""
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
throw new Error(`Unknown agent command: ${command}`);
|
|
1798
|
+
}
|
|
1799
|
+
function printAgentResult(command, result, json) {
|
|
1800
|
+
if (json) {
|
|
1801
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (command === "claim" && result && typeof result === "object" && "claim" in result) {
|
|
1805
|
+
const created = result;
|
|
1806
|
+
console.log(`Claimed ${created.claim?.target_id || "target"} as ${created.claim?.id || "claim"}`);
|
|
1807
|
+
if (created.claim_token) console.log(`Token: ${created.claim_token}`);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (command === "status" && result && typeof result === "object" && "claims" in result) {
|
|
1811
|
+
const status = result;
|
|
1812
|
+
if (status.claims.length === 0) {
|
|
1813
|
+
console.log("No agent claims.");
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
for (const claim of status.claims) console.log(`${claim.agent_name} ${claim.derived_state} ${claim.target_id}`);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (result && typeof result === "object" && "claim" in result) {
|
|
1820
|
+
const inspected = result;
|
|
1821
|
+
console.log(`${inspected.claim?.id || "claim"} ${inspected.claim?.status || "unknown"} ${inspected.claim?.target_id || ""}`.trim());
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
console.log(String(result));
|
|
1825
|
+
}
|
|
1325
1826
|
function start(config, args) {
|
|
1326
1827
|
let options;
|
|
1327
1828
|
const json = args.includes("--json");
|
|
@@ -1394,9 +1895,29 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
1394
1895
|
try {
|
|
1395
1896
|
printDataResult(command, runLineageDataCommand(command, commandArgs), json2);
|
|
1396
1897
|
} catch (error) {
|
|
1397
|
-
const message2 = error instanceof Error ? error.message : String(error);
|
|
1398
|
-
if (json2)
|
|
1399
|
-
|
|
1898
|
+
const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
|
|
1899
|
+
if (json2) {
|
|
1900
|
+
const output = isAgentClaimError(error) ? { ok: false, command, error: error.code, message: message2, conflicts: error.conflicts } : { ok: false, command, error: message2 };
|
|
1901
|
+
console.error(JSON.stringify(output, null, 2));
|
|
1902
|
+
} else console.error(`${config.binName}: ${message2}`);
|
|
1903
|
+
process.exit(1);
|
|
1904
|
+
}
|
|
1905
|
+
process.exit(0);
|
|
1906
|
+
}
|
|
1907
|
+
if (command === "agent") {
|
|
1908
|
+
const commandArgs = normalizedArgs.slice(2);
|
|
1909
|
+
const agentCommand = normalizedArgs[1] || "";
|
|
1910
|
+
const json2 = commandArgs.includes("--json");
|
|
1911
|
+
try {
|
|
1912
|
+
printAgentResult(agentCommand, runLineageAgentCommand(agentCommand, commandArgs), json2);
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
|
|
1915
|
+
if (json2) {
|
|
1916
|
+
const output = isAgentClaimError(error) ? { ok: false, command: `agent ${agentCommand}`, error: error.code, message: message2, conflicts: error.conflicts } : { ok: false, command: `agent ${agentCommand}`, error: message2 };
|
|
1917
|
+
console.error(JSON.stringify(output, null, 2));
|
|
1918
|
+
} else {
|
|
1919
|
+
console.error(`${config.binName}: ${message2}`);
|
|
1920
|
+
}
|
|
1400
1921
|
process.exit(1);
|
|
1401
1922
|
}
|
|
1402
1923
|
process.exit(0);
|