@rynx-ai/daemon 0.1.10 → 0.1.11-beta.10
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/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
- package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +25 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +56 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +506 -0
- package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/index.js +7 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
- package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
- package/bundled-plugins/plugins/lark/dist/lark.js +1975 -0
- package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/runtime.js +131328 -0
- package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/settings.js +126971 -0
- package/bundled-plugins/plugins/lark/package.json +57 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-D8Amoe6a.js +201 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/index.html +13 -0
- package/dist/agent-file.js +0 -1
- package/dist/browser-artifact-management.d.ts +4 -1
- package/dist/browser-artifact-management.js +18 -10
- package/dist/browser-artifacts.d.ts +1 -0
- package/dist/browser-artifacts.js +1 -0
- package/dist/bundled-plugins.d.ts +15 -0
- package/dist/bundled-plugins.js +134 -0
- package/dist/chrome-for-testing-runtime.d.ts +27 -0
- package/dist/chrome-for-testing-runtime.js +193 -0
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +9 -0
- package/dist/control-deps.d.ts +6 -2
- package/dist/control-deps.js +88 -82
- package/dist/daemon-server.d.ts +7 -1
- package/dist/daemon-server.js +310 -87
- package/dist/db.js +181 -124
- package/dist/entry-path.d.ts +3 -0
- package/dist/entry-path.js +12 -0
- package/dist/index-daemon.js +3 -66
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.js +16 -0
- package/dist/maintenance-lease.d.ts +39 -0
- package/dist/maintenance-lease.js +250 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
- package/dist/migrations/cleanup-legacy-sessions.js +33 -15
- package/dist/official-plugins.d.ts +2 -0
- package/dist/official-plugins.js +2 -0
- package/dist/plugin-cli.d.ts +1 -3
- package/dist/plugin-cli.js +56 -45
- package/dist/plugin-host-leases.d.ts +4 -0
- package/dist/plugin-host-leases.js +7 -0
- package/dist/plugin-host-rpc.d.ts +73 -6
- package/dist/plugin-host-rpc.js +354 -81
- package/dist/plugin-installer.d.ts +24 -55
- package/dist/plugin-installer.js +258 -412
- package/dist/plugin-management-service.d.ts +30 -23
- package/dist/plugin-management-service.js +288 -105
- package/dist/plugin-marketplace.d.ts +35 -0
- package/dist/plugin-marketplace.js +299 -0
- package/dist/plugin-package.d.ts +22 -24
- package/dist/plugin-package.js +520 -571
- package/dist/plugin-skill-context.d.ts +4 -0
- package/dist/plugin-skill-context.js +35 -0
- package/dist/plugin-store.d.ts +36 -37
- package/dist/plugin-store.js +138 -92
- package/dist/plugin-supervisor.d.ts +50 -125
- package/dist/plugin-supervisor.js +678 -1030
- package/dist/pm2.d.ts +11 -2
- package/dist/pm2.js +130 -28
- package/dist/process.d.ts +12 -0
- package/dist/process.js +91 -0
- package/dist/runtime-project-store.d.ts +9 -0
- package/dist/runtime-project-store.js +83 -0
- package/dist/session-fallback-workspace.d.ts +22 -0
- package/dist/session-fallback-workspace.js +120 -0
- package/dist/session-fork-store.d.ts +41 -0
- package/dist/session-fork-store.js +288 -0
- package/dist/session-launch-operations.d.ts +23 -2
- package/dist/session-launch-operations.js +48 -7
- package/dist/session-meta-store.d.ts +12 -9
- package/dist/session-meta-store.js +50 -49
- package/dist/session-portal-grant-store.d.ts +29 -0
- package/dist/session-portal-grant-store.js +84 -0
- package/dist/setup-service.d.ts +21 -0
- package/dist/setup-service.js +39 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +307 -7
- package/dist/system-dependencies.d.ts +26 -0
- package/dist/system-dependencies.js +149 -0
- package/package.json +36 -20
- package/dist/browser-cli-args.d.ts +0 -20
- package/dist/browser-cli-args.js +0 -100
- package/dist/channel-store.d.ts +0 -22
- package/dist/channel-store.js +0 -65
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -732
- package/dist/control-client.d.ts +0 -122
- package/dist/control-client.js +0 -855
- package/dist/control-endpoint.d.ts +0 -14
- package/dist/control-endpoint.js +0 -124
- package/dist/desktop-browser-host-client.d.ts +0 -35
- package/dist/desktop-browser-host-client.js +0 -415
- package/dist/instance-store.d.ts +0 -38
- package/dist/instance-store.js +0 -85
- package/dist/plugin-install-restart.d.ts +0 -13
- package/dist/plugin-install-restart.js +0 -24
- package/dist/plugin-runtime-control.d.ts +0 -2
- package/dist/plugin-runtime-control.js +0 -37
- package/dist/registry.d.ts +0 -10
- package/dist/registry.js +0 -52
- package/dist/setup.d.ts +0 -20
- package/dist/setup.js +0 -374
- package/dist/update.d.ts +0 -25
- package/dist/update.js +0 -133
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { db } from "./db.js";
|
|
3
|
+
const TICKET_TTL_MS = 5 * 60 * 1_000;
|
|
4
|
+
const GRANT_TTL_MS = 30 * 60 * 1_000;
|
|
5
|
+
/** Issue a browser handoff into the native Session workbench. The Host knows
|
|
6
|
+
* only the generic Session scope and access level; product/user policy stays in
|
|
7
|
+
* the plugin that asks for the ticket. */
|
|
8
|
+
export function issueSessionPortalTicket(input) {
|
|
9
|
+
if ((input.scope === "single_session") !== Boolean(input.sessionId)) {
|
|
10
|
+
throw new Error("single_session Portal tickets require exactly one Session id");
|
|
11
|
+
}
|
|
12
|
+
const now = input.now ?? new Date();
|
|
13
|
+
const expiresAt = new Date(now.getTime() + TICKET_TTL_MS).toISOString();
|
|
14
|
+
const ticket = randomToken();
|
|
15
|
+
pruneSessionPortalAuthorization(now);
|
|
16
|
+
db().prepare(`INSERT INTO session_portal_tickets
|
|
17
|
+
(ticket_hash, plugin_id, scope, access, session_id, created_at, expires_at, used_at)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`).run(hash(ticket), input.pluginId, input.scope, input.access, input.sessionId ?? null, now.toISOString(), expiresAt);
|
|
19
|
+
return {
|
|
20
|
+
path: `/session-portal/redeem#ticket=${encodeURIComponent(ticket)}`,
|
|
21
|
+
expiresAt,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Atomically consume a one-use ticket and mint a fixed-lifetime browser grant. */
|
|
25
|
+
export function redeemSessionPortalTicket(ticket, now = new Date()) {
|
|
26
|
+
const connection = db();
|
|
27
|
+
return connection.transaction(() => {
|
|
28
|
+
const ticketHash = hash(ticket);
|
|
29
|
+
const row = connection.prepare(`SELECT plugin_id, scope, access, session_id, expires_at, used_at
|
|
30
|
+
FROM session_portal_tickets
|
|
31
|
+
WHERE ticket_hash = ?`).get(ticketHash);
|
|
32
|
+
if (!row || row.used_at !== null || row.expires_at <= now.toISOString())
|
|
33
|
+
return undefined;
|
|
34
|
+
const consumed = connection.prepare(`UPDATE session_portal_tickets SET used_at = ?
|
|
35
|
+
WHERE ticket_hash = ? AND used_at IS NULL AND expires_at > ?`).run(now.toISOString(), ticketHash, now.toISOString());
|
|
36
|
+
if (consumed.changes !== 1)
|
|
37
|
+
return undefined;
|
|
38
|
+
const portalId = randomUUID();
|
|
39
|
+
const secret = randomToken();
|
|
40
|
+
const expiresAt = new Date(now.getTime() + GRANT_TTL_MS).toISOString();
|
|
41
|
+
connection.prepare(`INSERT INTO session_portal_grants
|
|
42
|
+
(portal_id, secret_hash, plugin_id, scope, access, session_id, created_at, expires_at, revoked_at)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(portalId, hash(secret), row.plugin_id, row.scope, row.access, row.session_id, now.toISOString(), expiresAt);
|
|
44
|
+
return {
|
|
45
|
+
portalId,
|
|
46
|
+
secret,
|
|
47
|
+
pluginId: row.plugin_id,
|
|
48
|
+
scope: row.scope,
|
|
49
|
+
access: row.access,
|
|
50
|
+
...(row.session_id ? { sessionId: row.session_id } : {}),
|
|
51
|
+
expiresAt,
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
}
|
|
55
|
+
export function authorizeSessionPortalGrant(portalId, secret, now = new Date()) {
|
|
56
|
+
const row = db().prepare(`SELECT portal_id, secret_hash, plugin_id, scope, access, session_id, expires_at, revoked_at
|
|
57
|
+
FROM session_portal_grants
|
|
58
|
+
WHERE portal_id = ?`).get(portalId);
|
|
59
|
+
if (!row || row.revoked_at !== null || row.expires_at <= now.toISOString())
|
|
60
|
+
return undefined;
|
|
61
|
+
const actual = Buffer.from(hash(secret), "hex");
|
|
62
|
+
const expected = Buffer.from(row.secret_hash, "hex");
|
|
63
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
|
|
64
|
+
return undefined;
|
|
65
|
+
return {
|
|
66
|
+
portalId: row.portal_id,
|
|
67
|
+
pluginId: row.plugin_id,
|
|
68
|
+
scope: row.scope,
|
|
69
|
+
access: row.access,
|
|
70
|
+
...(row.session_id ? { sessionId: row.session_id } : {}),
|
|
71
|
+
expiresAt: row.expires_at,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function pruneSessionPortalAuthorization(now = new Date()) {
|
|
75
|
+
const connection = db();
|
|
76
|
+
connection.prepare("DELETE FROM session_portal_tickets WHERE expires_at <= ? OR used_at IS NOT NULL").run(now.toISOString());
|
|
77
|
+
connection.prepare("DELETE FROM session_portal_grants WHERE expires_at <= ? OR revoked_at IS NOT NULL").run(now.toISOString());
|
|
78
|
+
}
|
|
79
|
+
function randomToken() {
|
|
80
|
+
return randomBytes(32).toString("base64url");
|
|
81
|
+
}
|
|
82
|
+
function hash(value) {
|
|
83
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { ensureSystemDependencies, inspectSystemDependencies, type SystemDependencySnapshot, } from "./system-dependencies.js";
|
|
2
|
+
export interface DaemonDiagnosticSnapshot {
|
|
3
|
+
databasePath: string;
|
|
4
|
+
databaseExists: boolean;
|
|
5
|
+
pluginCount: number;
|
|
6
|
+
agentCount: number;
|
|
7
|
+
browser: {
|
|
8
|
+
supported: false;
|
|
9
|
+
} | {
|
|
10
|
+
supported: true;
|
|
11
|
+
installedVersion?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function prepareBundledPlugins(): Promise<{
|
|
16
|
+
status: "ready" | "unavailable";
|
|
17
|
+
installed: string[];
|
|
18
|
+
updated: string[];
|
|
19
|
+
unchanged: string[];
|
|
20
|
+
}>;
|
|
21
|
+
export declare function inspectDaemonDiagnostics(): Promise<DaemonDiagnosticSnapshot>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { listAgentSpecs } from "@rynx-ai/core";
|
|
3
|
+
import { createChromeForTestingArtifactStore, isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
|
|
4
|
+
import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
|
|
5
|
+
import { dbPath } from "./db.js";
|
|
6
|
+
import { listPlugins } from "./plugin-store.js";
|
|
7
|
+
export { ensureSystemDependencies, inspectSystemDependencies, } from "./system-dependencies.js";
|
|
8
|
+
export async function prepareBundledPlugins() {
|
|
9
|
+
return ensureBundledOfficialPlugins();
|
|
10
|
+
}
|
|
11
|
+
export async function inspectDaemonDiagnostics() {
|
|
12
|
+
const databasePath = dbPath();
|
|
13
|
+
let browser;
|
|
14
|
+
if (!isChromeForTestingHostSupported()) {
|
|
15
|
+
browser = { supported: false };
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
try {
|
|
19
|
+
const installed = createChromeForTestingArtifactStore().readActive();
|
|
20
|
+
browser = {
|
|
21
|
+
supported: true,
|
|
22
|
+
...(installed ? { installedVersion: installed.version } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
browser = {
|
|
27
|
+
supported: true,
|
|
28
|
+
error: error instanceof Error ? error.message : String(error),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
databasePath,
|
|
34
|
+
databaseExists: existsSync(databasePath),
|
|
35
|
+
pluginCount: existsSync(databasePath) ? listPlugins().length : 0,
|
|
36
|
+
agentCount: (await listAgentSpecs()).length,
|
|
37
|
+
browser,
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/skills-catalog.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
|
|
2
|
-
import type { SkillDetail } from "@rynx-ai/protocol/control";
|
|
1
|
+
import { type LocalSkillRuntime, type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
|
|
2
|
+
import type { ControlLocalSkillCandidate, ControlLocalSkillImportResult, SkillDetail } from "@rynx-ai/protocol/control";
|
|
3
3
|
export declare function listCatalogSkills(): Promise<SkillMeta[]>;
|
|
4
4
|
/** Install a skill (or a repo's skills) into the catalog per a full install
|
|
5
5
|
* recipe; delegates to the core catalog layer (which also writes each skill's
|
|
6
6
|
* `.rynx-skill.json` install-info file — a catalog-only artifact). Returns
|
|
7
7
|
* the refreshed catalog. */
|
|
8
8
|
export declare function installCatalogSkill(recipe: SkillInstallRecipe): Promise<SkillMeta[]>;
|
|
9
|
+
export declare function scanLocalCatalogSkillCandidates(runtime: LocalSkillRuntime): Promise<ControlLocalSkillCandidate[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Register complete local runtime Skill bundles into the owner catalog. Every
|
|
12
|
+
* content hash gets a durable immutable artifact; the mutable catalog entry's
|
|
13
|
+
* receipt points at that artifact so old Agent specs keep their exact bytes
|
|
14
|
+
* after a later catalog update.
|
|
15
|
+
*/
|
|
16
|
+
export declare function importLocalCatalogSkills(runtime: LocalSkillRuntime, keys: string[]): Promise<ControlLocalSkillImportResult[]>;
|
|
17
|
+
export declare function importCatalogSkillsFromArchive(filename: string, contentBase64: string): Promise<ControlLocalSkillImportResult[]>;
|
|
9
18
|
/** Detail for one catalog skill: its metadata + the file tree under its dir. */
|
|
10
19
|
export declare function getCatalogSkill(name: string): Promise<SkillDetail | null>;
|
|
11
20
|
/** Read one text file inside a catalog skill. Path-confined to the skill dir,
|
package/dist/skills-catalog.js
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
* env-build share one implementation; this module is the daemon-facing wrapper
|
|
5
5
|
* plus the read-only detail/file helpers the Skills page uses.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
8
10
|
import path from "node:path";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
11
|
+
import extractZipArchive from "extract-zip";
|
|
12
|
+
import { extractSkillFrontmatter, installIntoCatalog, collectLocalSkillBundlesInWorker, collectSkillBundlesFromDirectory, scanLocalSkillCandidatesInWorker, rynxSkillsDir, rynxSkillArtifactDir, rynxSkillArtifactsDir, scanSkillsDir, SAFE_SKILL_NAME, readSkillMeta, writeSkillInstallInfo, } from "@rynx-ai/core";
|
|
12
13
|
/** A single file's contents are capped to keep responses small. */
|
|
13
14
|
const MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
15
|
+
const MAX_ARCHIVE_ENTRIES = 1_000;
|
|
16
|
+
const MAX_ARCHIVE_EXTRACTED_BYTES = 24 * 1024 * 1024;
|
|
17
|
+
/** Concurrent Control API requests for the same Skill are serialized so the
|
|
18
|
+
* catalog's backup/swap transaction cannot interleave. */
|
|
19
|
+
const localRegistrationTails = new Map();
|
|
14
20
|
export function listCatalogSkills() {
|
|
15
21
|
return listMergedSkills();
|
|
16
22
|
}
|
|
@@ -22,9 +28,91 @@ export async function installCatalogSkill(recipe) {
|
|
|
22
28
|
await installIntoCatalog(recipe, rynxSkillsDir());
|
|
23
29
|
return listMergedSkills();
|
|
24
30
|
}
|
|
31
|
+
export function scanLocalCatalogSkillCandidates(runtime) {
|
|
32
|
+
return scanLocalSkillCandidatesInWorker(runtime);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register complete local runtime Skill bundles into the owner catalog. Every
|
|
36
|
+
* content hash gets a durable immutable artifact; the mutable catalog entry's
|
|
37
|
+
* receipt points at that artifact so old Agent specs keep their exact bytes
|
|
38
|
+
* after a later catalog update.
|
|
39
|
+
*/
|
|
40
|
+
export async function importLocalCatalogSkills(runtime, keys) {
|
|
41
|
+
const collected = await collectLocalSkillBundlesInWorker(runtime, keys);
|
|
42
|
+
return registerCollectedBundles(collected);
|
|
43
|
+
}
|
|
44
|
+
export async function importCatalogSkillsFromArchive(filename, contentBase64) {
|
|
45
|
+
if (!filename.toLowerCase().endsWith(".zip"))
|
|
46
|
+
throw new Error("archive must be a .zip file");
|
|
47
|
+
const content = Buffer.from(contentBase64, "base64");
|
|
48
|
+
if (content.byteLength === 0)
|
|
49
|
+
throw new Error("archive is empty");
|
|
50
|
+
if (content.byteLength > 20 * 1024 * 1024)
|
|
51
|
+
throw new Error("archive exceeds the 20 MB limit");
|
|
52
|
+
const staging = await mkdtemp(path.join(tmpdir(), "rynx-skill-archive-"));
|
|
53
|
+
const archive = path.join(staging, "upload.zip");
|
|
54
|
+
const extracted = path.join(staging, "extracted");
|
|
55
|
+
try {
|
|
56
|
+
await writeFile(archive, content);
|
|
57
|
+
await mkdir(extracted, { recursive: true });
|
|
58
|
+
let entryCount = 0;
|
|
59
|
+
let extractedBytes = 0;
|
|
60
|
+
await extractZipArchive(archive, {
|
|
61
|
+
dir: extracted,
|
|
62
|
+
onEntry: (entry) => {
|
|
63
|
+
entryCount += 1;
|
|
64
|
+
extractedBytes += entry.uncompressedSize;
|
|
65
|
+
if (entryCount > MAX_ARCHIVE_ENTRIES) {
|
|
66
|
+
throw new Error(`archive exceeds the ${MAX_ARCHIVE_ENTRIES}-entry limit`);
|
|
67
|
+
}
|
|
68
|
+
if (extractedBytes > MAX_ARCHIVE_EXTRACTED_BYTES) {
|
|
69
|
+
throw new Error("archive exceeds the 24 MB extracted-size limit");
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const collected = collectSkillBundlesFromDirectory(extracted);
|
|
74
|
+
if (collected.bundles.length === 0 && collected.failures.length === 0) {
|
|
75
|
+
return [{
|
|
76
|
+
status: "failed",
|
|
77
|
+
name: path.basename(filename, path.extname(filename)),
|
|
78
|
+
sourcePath: filename,
|
|
79
|
+
message: "No valid Skill bundle was found in the archive.",
|
|
80
|
+
}];
|
|
81
|
+
}
|
|
82
|
+
return registerCollectedBundles({
|
|
83
|
+
bundles: collected.bundles.map((bundle) => ({ ...bundle, sourcePath: filename })),
|
|
84
|
+
failures: collected.failures.map((failure) => ({ ...failure, sourcePath: filename })),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await rm(staging, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function registerCollectedBundles(collected) {
|
|
92
|
+
const results = collected.failures.map((failure) => ({
|
|
93
|
+
status: "failed",
|
|
94
|
+
name: failure.name,
|
|
95
|
+
...(failure.sourcePath ? { sourcePath: failure.sourcePath } : {}),
|
|
96
|
+
message: failure.message,
|
|
97
|
+
}));
|
|
98
|
+
for (const bundle of collected.bundles) {
|
|
99
|
+
try {
|
|
100
|
+
results.push(await registerLocalBundle(bundle));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
results.push({
|
|
104
|
+
status: "failed",
|
|
105
|
+
name: bundle.name,
|
|
106
|
+
sourcePath: bundle.sourcePath,
|
|
107
|
+
message: error instanceof Error ? error.message : String(error),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
25
113
|
/** Detail for one catalog skill: its metadata + the file tree under its dir. */
|
|
26
114
|
export async function getCatalogSkill(name) {
|
|
27
|
-
if (!
|
|
115
|
+
if (!SAFE_SKILL_NAME.test(name))
|
|
28
116
|
throw new Error(`invalid skill name: ${name}`);
|
|
29
117
|
const dir = path.join(rynxSkillsDir(), name);
|
|
30
118
|
const meta = await readSkillMeta(dir);
|
|
@@ -62,7 +150,7 @@ async function walkSkillFiles(root, dir) {
|
|
|
62
150
|
/** Read one text file inside a catalog skill. Path-confined to the skill dir,
|
|
63
151
|
* size-capped, and binary files are rejected. */
|
|
64
152
|
export async function readCatalogSkillFile(name, relPath) {
|
|
65
|
-
if (!
|
|
153
|
+
if (!SAFE_SKILL_NAME.test(name))
|
|
66
154
|
throw new Error(`invalid skill name: ${name}`);
|
|
67
155
|
const dir = path.resolve(path.join(rynxSkillsDir(), name));
|
|
68
156
|
const target = path.resolve(dir, relPath);
|
|
@@ -82,9 +170,221 @@ export async function readCatalogSkillFile(name, relPath) {
|
|
|
82
170
|
async function listMergedSkills() {
|
|
83
171
|
return (await scanSkillsDir(rynxSkillsDir())).sort((a, b) => a.name.localeCompare(b.name));
|
|
84
172
|
}
|
|
173
|
+
async function registerLocalBundle(bundle) {
|
|
174
|
+
const previous = localRegistrationTails.get(bundle.name) ?? Promise.resolve();
|
|
175
|
+
const pending = previous.catch(() => undefined).then(() => registerLocalBundleUnlocked(bundle));
|
|
176
|
+
localRegistrationTails.set(bundle.name, pending);
|
|
177
|
+
try {
|
|
178
|
+
return await pending;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
if (localRegistrationTails.get(bundle.name) === pending) {
|
|
182
|
+
localRegistrationTails.delete(bundle.name);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function registerLocalBundleUnlocked(bundle) {
|
|
187
|
+
const catalogRoot = rynxSkillsDir();
|
|
188
|
+
const currentDir = path.join(catalogRoot, bundle.name);
|
|
189
|
+
const existing = await readSkillMeta(currentDir);
|
|
190
|
+
if (existing?.ref && !isManagedLocalArtifact(existing.ref.install)) {
|
|
191
|
+
return {
|
|
192
|
+
status: "conflict",
|
|
193
|
+
name: bundle.name,
|
|
194
|
+
sourcePath: bundle.sourcePath,
|
|
195
|
+
skill: toSummary(existing),
|
|
196
|
+
message: "A Skill with this name is registered from another source.",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (existing?.ref?.contentHash === bundle.contentHash) {
|
|
200
|
+
return {
|
|
201
|
+
status: "skipped",
|
|
202
|
+
name: bundle.name,
|
|
203
|
+
sourcePath: bundle.sourcePath,
|
|
204
|
+
skill: toSummary(existing),
|
|
205
|
+
message: "Skill is already up to date.",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (await pathExists(currentDir) && !existing) {
|
|
209
|
+
return {
|
|
210
|
+
status: "conflict",
|
|
211
|
+
name: bundle.name,
|
|
212
|
+
sourcePath: bundle.sourcePath,
|
|
213
|
+
message: "A receipt-less Skill with this name already exists. Remove it before importing.",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const artifactDir = rynxSkillArtifactDir(bundle.name, bundle.contentHash);
|
|
217
|
+
await materializeArtifact(bundle, artifactDir);
|
|
218
|
+
const ref = {
|
|
219
|
+
name: bundle.name,
|
|
220
|
+
install: { tool: "skills-cli", source: artifactDir },
|
|
221
|
+
contentHash: bundle.contentHash,
|
|
222
|
+
};
|
|
223
|
+
await mkdir(catalogRoot, { recursive: true });
|
|
224
|
+
const stagingRoot = await mkdtemp(path.join(catalogRoot, `.import-${bundle.name}-`));
|
|
225
|
+
const stagedDir = path.join(stagingRoot, bundle.name);
|
|
226
|
+
const backupDir = path.join(catalogRoot, `.backup-${bundle.name}-${process.pid}-${randomUUID()}`);
|
|
227
|
+
let backedUp = false;
|
|
228
|
+
try {
|
|
229
|
+
await cp(artifactDir, stagedDir, { recursive: true });
|
|
230
|
+
await writeSkillInstallInfo(stagedDir, ref);
|
|
231
|
+
if (await pathExists(currentDir)) {
|
|
232
|
+
await rename(currentDir, backupDir);
|
|
233
|
+
backedUp = true;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
await rename(stagedDir, currentDir);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (backedUp) {
|
|
240
|
+
await rename(backupDir, currentDir);
|
|
241
|
+
backedUp = false;
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
if (backedUp) {
|
|
246
|
+
await rm(backupDir, { recursive: true, force: true });
|
|
247
|
+
backedUp = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
252
|
+
if (backedUp && !(await pathExists(currentDir))) {
|
|
253
|
+
await rename(backupDir, currentDir);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const meta = await readSkillMeta(currentDir);
|
|
257
|
+
if (!meta)
|
|
258
|
+
throw new Error("Imported Skill failed catalog validation");
|
|
259
|
+
return {
|
|
260
|
+
status: existing ? "updated" : "created",
|
|
261
|
+
name: meta.name,
|
|
262
|
+
sourcePath: bundle.sourcePath,
|
|
263
|
+
skill: toSummary(meta),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function materializeArtifact(bundle, artifactDir) {
|
|
267
|
+
if (await pathExists(artifactDir)) {
|
|
268
|
+
if (await artifactMatchesBundle(bundle, artifactDir))
|
|
269
|
+
return;
|
|
270
|
+
throw new Error("Immutable Skill artifact already exists but does not match its content hash");
|
|
271
|
+
}
|
|
272
|
+
const artifactsRoot = rynxSkillArtifactsDir();
|
|
273
|
+
await mkdir(artifactsRoot, { recursive: true });
|
|
274
|
+
const stagingRoot = await mkdtemp(path.join(artifactsRoot, `.artifact-${bundle.name}-`));
|
|
275
|
+
const stagedDir = path.join(stagingRoot, bundle.name);
|
|
276
|
+
try {
|
|
277
|
+
await mkdir(stagedDir, { recursive: true });
|
|
278
|
+
const aggregate = createHash("sha256");
|
|
279
|
+
for (const file of [...bundle.files].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
280
|
+
const relative = normalizeBundlePath(file.path);
|
|
281
|
+
const content = Buffer.from(file.contentBase64, "base64");
|
|
282
|
+
if (content.byteLength !== file.sizeBytes)
|
|
283
|
+
throw new Error(`Skill file size mismatch: ${relative}`);
|
|
284
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
285
|
+
if (sha256 !== file.sha256)
|
|
286
|
+
throw new Error(`Skill file hash mismatch: ${relative}`);
|
|
287
|
+
aggregate.update(relative);
|
|
288
|
+
aggregate.update("\0");
|
|
289
|
+
aggregate.update(sha256);
|
|
290
|
+
aggregate.update("\0");
|
|
291
|
+
aggregate.update(String(content.byteLength));
|
|
292
|
+
aggregate.update("\0");
|
|
293
|
+
const target = path.join(stagedDir, ...relative.split("/"));
|
|
294
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
295
|
+
await writeFile(target, content);
|
|
296
|
+
}
|
|
297
|
+
if (aggregate.digest("hex") !== bundle.contentHash)
|
|
298
|
+
throw new Error("Skill bundle hash mismatch");
|
|
299
|
+
const parent = path.dirname(artifactDir);
|
|
300
|
+
await mkdir(parent, { recursive: true });
|
|
301
|
+
try {
|
|
302
|
+
await rename(stagedDir, artifactDir);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (await artifactMatchesBundle(bundle, artifactDir))
|
|
306
|
+
return;
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function artifactMatchesBundle(bundle, artifactDir) {
|
|
315
|
+
try {
|
|
316
|
+
const actualFiles = await walkArtifactFiles(artifactDir, artifactDir);
|
|
317
|
+
const expectedFiles = [...bundle.files].sort((left, right) => left.path.localeCompare(right.path));
|
|
318
|
+
if (!actualFiles || actualFiles.length !== expectedFiles.length)
|
|
319
|
+
return false;
|
|
320
|
+
for (let index = 0; index < expectedFiles.length; index += 1) {
|
|
321
|
+
const expected = expectedFiles[index];
|
|
322
|
+
if (actualFiles[index] !== expected.path)
|
|
323
|
+
return false;
|
|
324
|
+
const content = await readFile(path.join(artifactDir, ...expected.path.split("/")));
|
|
325
|
+
if (content.byteLength !== expected.sizeBytes)
|
|
326
|
+
return false;
|
|
327
|
+
if (createHash("sha256").update(content).digest("hex") !== expected.sha256)
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function walkArtifactFiles(root, dir) {
|
|
337
|
+
const files = [];
|
|
338
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
339
|
+
if (entry.isSymbolicLink())
|
|
340
|
+
return null;
|
|
341
|
+
const absolute = path.join(dir, entry.name);
|
|
342
|
+
if (entry.isDirectory()) {
|
|
343
|
+
const nested = await walkArtifactFiles(root, absolute);
|
|
344
|
+
if (!nested)
|
|
345
|
+
return null;
|
|
346
|
+
files.push(...nested);
|
|
347
|
+
}
|
|
348
|
+
else if (entry.isFile()) {
|
|
349
|
+
files.push(normalizeBundlePath(path.relative(root, absolute)));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
353
|
+
}
|
|
354
|
+
function normalizeBundlePath(value) {
|
|
355
|
+
const parts = value.replace(/\\/g, "/").split("/");
|
|
356
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
357
|
+
throw new Error(`Invalid Skill file path: ${value}`);
|
|
358
|
+
}
|
|
359
|
+
return parts.join("/");
|
|
360
|
+
}
|
|
361
|
+
function isManagedLocalArtifact(recipe) {
|
|
362
|
+
if (recipe.tool !== "skills-cli" || !path.isAbsolute(recipe.source))
|
|
363
|
+
return false;
|
|
364
|
+
const root = path.resolve(rynxSkillArtifactsDir());
|
|
365
|
+
const source = path.resolve(recipe.source);
|
|
366
|
+
const relative = path.relative(root, source);
|
|
367
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
368
|
+
}
|
|
369
|
+
function toSummary(meta) {
|
|
370
|
+
return {
|
|
371
|
+
name: meta.name,
|
|
372
|
+
description: meta.description,
|
|
373
|
+
...(meta.ref ? { ref: meta.ref } : {}),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async function pathExists(value) {
|
|
377
|
+
try {
|
|
378
|
+
await stat(value);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
85
385
|
/** Remove a catalog skill by name. Returns false if it wasn't present. */
|
|
86
386
|
export async function removeCatalogSkill(name) {
|
|
87
|
-
if (!
|
|
387
|
+
if (!SAFE_SKILL_NAME.test(name))
|
|
88
388
|
throw new Error(`invalid skill name: ${name}`);
|
|
89
389
|
const dir = path.join(rynxSkillsDir(), name);
|
|
90
390
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SystemDependencyStatus {
|
|
2
|
+
installed: boolean;
|
|
3
|
+
detail?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SystemDependencySnapshot {
|
|
6
|
+
tmux: SystemDependencyStatus;
|
|
7
|
+
}
|
|
8
|
+
export interface SystemDependencyCommandResult {
|
|
9
|
+
exitCode: number | null;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SystemDependencyCommandOptions {
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
}
|
|
17
|
+
export type SystemDependencyCommandRunner = (command: string, args: readonly string[], options: SystemDependencyCommandOptions) => Promise<SystemDependencyCommandResult>;
|
|
18
|
+
export interface SystemDependencyOptions {
|
|
19
|
+
platform?: string;
|
|
20
|
+
getuid?: () => number | undefined;
|
|
21
|
+
findSystemExecutable?: (name: string) => Promise<string | undefined>;
|
|
22
|
+
runCommand?: SystemDependencyCommandRunner;
|
|
23
|
+
onProgress?: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function inspectSystemDependencies(options?: Pick<SystemDependencyOptions, "runCommand">): Promise<SystemDependencySnapshot>;
|
|
26
|
+
export declare function ensureSystemDependencies(options?: SystemDependencyOptions): Promise<SystemDependencySnapshot>;
|