@sema-agent/server 1.308.0 → 1.309.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.
@@ -0,0 +1,63 @@
1
+ import type { Logger } from "../observability/logger.js";
2
+ import { type LoadedSkill } from "./skills.js";
3
+ export declare const DEFAULT_PLUGIN_ALLOW_HOSTS: readonly string[];
4
+ export interface PluginDeclWire {
5
+ name?: unknown;
6
+ source?: {
7
+ kind?: unknown;
8
+ url?: unknown;
9
+ marketplace?: unknown;
10
+ };
11
+ version?: unknown;
12
+ sha?: unknown;
13
+ enabled?: unknown;
14
+ }
15
+ export type PluginValidation = {
16
+ ok: true;
17
+ name: string;
18
+ url: string;
19
+ sha?: string;
20
+ } | {
21
+ ok: false;
22
+ skip?: true;
23
+ name: string;
24
+ reason: string;
25
+ };
26
+ export declare function validatePluginDecl(decl: PluginDeclWire, allowHosts: readonly string[]): PluginValidation;
27
+ export declare function gitClonePlan(o: {
28
+ url: string;
29
+ dest: string;
30
+ sha?: string;
31
+ }): {
32
+ steps: string[][];
33
+ env: Record<string, string>;
34
+ };
35
+ export declare function resolvePluginSkillsRoot(cloneRoot: string): {
36
+ ok: true;
37
+ dir?: string;
38
+ } | {
39
+ ok: false;
40
+ reason: string;
41
+ };
42
+ export interface ApplyPluginsResult {
43
+ skills: LoadedSkill[];
44
+ failures: Array<{
45
+ plugin: string;
46
+ reason: string;
47
+ }>;
48
+ letGo: Array<{
49
+ plugin: string;
50
+ skill: string;
51
+ }>;
52
+ lkgUsed: string[];
53
+ }
54
+ export declare function applyCenterPlugins(baseline: LoadedSkill[], eff: {
55
+ plugins?: unknown;
56
+ }, opts: {
57
+ cacheRoot: string;
58
+ allowHosts: readonly string[];
59
+ logger?: Logger;
60
+ skipValidate?: boolean;
61
+ runGit?: (args: string[], env: Record<string, string>) => Promise<void>;
62
+ }): Promise<ApplyPluginsResult>;
63
+ //# sourceMappingURL=center-plugins.d.ts.map
@@ -0,0 +1,163 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { promises as fsp, realpathSync, existsSync, readFileSync } from "node:fs";
4
+ import { join, resolve, isAbsolute, sep } from "node:path";
5
+ import { loadSkills } from "./skills.js";
6
+ const execFileP = promisify(execFile);
7
+ export const DEFAULT_PLUGIN_ALLOW_HOSTS = ["github.com"];
8
+ export function validatePluginDecl(decl, allowHosts) {
9
+ const name = typeof decl.name === "string" && decl.name !== "" ? decl.name : "(unnamed)";
10
+ if (decl.enabled === false)
11
+ return { ok: false, skip: true, name, reason: "enabled:false" };
12
+ const kind = decl.source?.kind;
13
+ if (kind === "marketplace") {
14
+ return { ok: false, skip: true, name, reason: "marketplace source: not materialized in v1 (skills lane ships git-form first)" };
15
+ }
16
+ if (kind !== "git")
17
+ return { ok: false, name, reason: `unsupported source.kind ${String(kind)}` };
18
+ const url = decl.source?.url;
19
+ if (typeof url !== "string" || !/^https:\/\//i.test(url)) {
20
+ return { ok: false, name, reason: `plugin source must be an https:// URL (contract: https-only; got ${String(url).slice(0, 80)})` };
21
+ }
22
+ let host;
23
+ try {
24
+ host = new URL(url).hostname.toLowerCase();
25
+ }
26
+ catch {
27
+ return { ok: false, name, reason: "https url does not parse" };
28
+ }
29
+ if (!allowHosts.some((h) => h.toLowerCase() === host)) {
30
+ return { ok: false, name, reason: `host ${host} not in PLUGINS_ALLOW_HOSTS (default github.com; extend with a comma-separated https mirror domain list)` };
31
+ }
32
+ if (decl.sha !== undefined) {
33
+ if (typeof decl.sha !== "string" || !/^[a-f0-9]{40}$/.test(decl.sha)) {
34
+ return { ok: false, name, reason: `sha must be a full 40-hex lowercase commit (got ${String(decl.sha).slice(0, 50)})` };
35
+ }
36
+ return { ok: true, name, url, sha: decl.sha };
37
+ }
38
+ return { ok: true, name, url };
39
+ }
40
+ export function gitClonePlan(o) {
41
+ const env = {
42
+ GIT_CONFIG_GLOBAL: "/dev/null",
43
+ GIT_CONFIG_SYSTEM: "/dev/null",
44
+ GIT_TERMINAL_PROMPT: "0",
45
+ };
46
+ if (o.sha === undefined) {
47
+ return { steps: [["clone", "--depth", "1", "--quiet", o.url, o.dest]], env };
48
+ }
49
+ return {
50
+ steps: [
51
+ ["init", "--quiet", o.dest],
52
+ ["-C", o.dest, "fetch", "--depth", "1", "--quiet", o.url, o.sha],
53
+ ["-C", o.dest, "checkout", "--quiet", o.sha],
54
+ ],
55
+ env,
56
+ };
57
+ }
58
+ export function resolvePluginSkillsRoot(cloneRoot) {
59
+ let skillsPath = "skills";
60
+ try {
61
+ const manifest = JSON.parse(readFileSync(join(cloneRoot, ".claude-plugin", "plugin.json"), "utf8"));
62
+ if (typeof manifest.skillsPath === "string" && manifest.skillsPath !== "")
63
+ skillsPath = manifest.skillsPath;
64
+ }
65
+ catch {
66
+ }
67
+ if (isAbsolute(skillsPath))
68
+ return { ok: false, reason: `skillsPath must be relative (got ${skillsPath})` };
69
+ const rootReal = realpathSync(cloneRoot);
70
+ const candidate = resolve(rootReal, skillsPath);
71
+ if (candidate !== rootReal && !candidate.startsWith(rootReal + sep)) {
72
+ return { ok: false, reason: `skillsPath escapes the plugin root (${skillsPath})` };
73
+ }
74
+ if (!existsSync(candidate))
75
+ return { ok: true };
76
+ const real = realpathSync(candidate);
77
+ if (real !== rootReal && !real.startsWith(rootReal + sep)) {
78
+ return { ok: false, reason: `skillsPath resolves outside the plugin root via symlink (${skillsPath})` };
79
+ }
80
+ return { ok: true, dir: candidate };
81
+ }
82
+ export async function applyCenterPlugins(baseline, eff, opts) {
83
+ const declared = Array.isArray(eff.plugins)
84
+ ? (eff.plugins)
85
+ : Array.isArray(eff.plugins?.plugins)
86
+ ? (eff.plugins.plugins)
87
+ : [];
88
+ const out = { skills: [...baseline], failures: [], letGo: [], lkgUsed: [] };
89
+ if (declared.length === 0)
90
+ return out;
91
+ const runGit = opts.runGit ?? (async (args, env) => {
92
+ await execFileP("git", args, { env: { ...process.env, ...env }, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
93
+ });
94
+ const taken = new Set(baseline.map((s) => s.spec.name));
95
+ for (const decl of declared) {
96
+ const v = opts.skipValidate
97
+ ? { ok: true, name: String(decl.name), url: String(decl.source?.url), ...(typeof decl.sha === "string" ? { sha: decl.sha } : {}) }
98
+ : validatePluginDecl(decl, opts.allowHosts);
99
+ if (!v.ok) {
100
+ if (v.skip)
101
+ opts.logger?.info("center_plugin_skipped", { plugin: v.name, reason: v.reason });
102
+ else {
103
+ opts.logger?.warn("center_plugin_rejected", { plugin: v.name, reason: v.reason });
104
+ out.failures.push({ plugin: v.name, reason: v.reason });
105
+ }
106
+ continue;
107
+ }
108
+ const liveDir = join(opts.cacheRoot, v.name, "live");
109
+ const staging = join(opts.cacheRoot, v.name, `staging-${process.pid}`);
110
+ let materialized = false;
111
+ try {
112
+ await fsp.rm(staging, { recursive: true, force: true });
113
+ await fsp.mkdir(staging, { recursive: true });
114
+ const plan = gitClonePlan({ url: v.url, dest: staging, sha: v.sha });
115
+ for (const step of plan.steps)
116
+ await runGit(step, plan.env);
117
+ await fsp.rm(liveDir, { recursive: true, force: true });
118
+ await fsp.mkdir(join(opts.cacheRoot, v.name), { recursive: true });
119
+ await fsp.rename(staging, liveDir);
120
+ materialized = true;
121
+ }
122
+ catch (err) {
123
+ await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
124
+ if (existsSync(liveDir)) {
125
+ opts.logger?.warn("center_plugin_fetch_failed_lkg", { plugin: v.name, error: err instanceof Error ? err.message : String(err) });
126
+ out.lkgUsed.push(v.name);
127
+ }
128
+ else {
129
+ opts.logger?.warn("center_plugin_fetch_failed", { plugin: v.name, error: err instanceof Error ? err.message : String(err) });
130
+ out.failures.push({ plugin: v.name, reason: `fetch failed: ${err instanceof Error ? err.message : String(err)}` });
131
+ continue;
132
+ }
133
+ }
134
+ try {
135
+ const rootCheck = resolvePluginSkillsRoot(liveDir);
136
+ if (!rootCheck.ok) {
137
+ out.failures.push({ plugin: v.name, reason: rootCheck.reason });
138
+ opts.logger?.warn("center_plugin_manifest_rejected", { plugin: v.name, reason: rootCheck.reason });
139
+ continue;
140
+ }
141
+ if (!rootCheck.dir)
142
+ continue;
143
+ const loaded = loadSkills(rootCheck.dir);
144
+ for (const skill of loaded) {
145
+ if (taken.has(skill.spec.name)) {
146
+ out.letGo.push({ plugin: v.name, skill: skill.spec.name });
147
+ opts.logger?.warn("center_plugin_skill_shadowed", { plugin: v.name, skill: skill.spec.name, note: "name taken by builtin/center skill — plugin yields (contract tightening-4)" });
148
+ continue;
149
+ }
150
+ taken.add(skill.spec.name);
151
+ out.skills.push(skill);
152
+ }
153
+ if (materialized)
154
+ opts.logger?.info("center_plugin_loaded", { plugin: v.name, skills: loaded.length, pinned: v.sha !== undefined });
155
+ }
156
+ catch (err) {
157
+ out.failures.push({ plugin: v.name, reason: `load failed: ${err instanceof Error ? err.message : String(err)}` });
158
+ opts.logger?.warn("center_plugin_load_failed", { plugin: v.name, error: err instanceof Error ? err.message : String(err) });
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+ //# sourceMappingURL=center-plugins.js.map
package/dist/config.d.ts CHANGED
@@ -164,6 +164,7 @@ export interface ServiceConfig {
164
164
  };
165
165
  snapshotBlobSqlMaxBytes?: number;
166
166
  snapshotBlobAllowSql: boolean;
167
+ pluginsAllowHosts: string[];
167
168
  bindHost?: string;
168
169
  attachmentOrphanGraceMs: number;
169
170
  workspaceFileMaxBytes: number;
package/dist/config.js CHANGED
@@ -538,6 +538,7 @@ export function loadConfig() {
538
538
  : undefined,
539
539
  snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
540
540
  snapshotBlobAllowSql: process.env.SNAPSHOT_BLOB_ALLOW_SQL_BYTES === "true",
541
+ pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
541
542
  bindHost: process.env.BIND_HOST || process.env.HOST || undefined,
542
543
  attachmentOrphanGraceMs: ((v) => (v !== undefined && Number.isFinite(v) && v >= 0 ? v : 3_600_000))(process.env.ATTACHMENT_ORPHAN_GRACE_MS !== undefined ? Number(process.env.ATTACHMENT_ORPHAN_GRACE_MS) : undefined),
543
544
  workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
@@ -634,7 +634,7 @@ export function createHttpServer(deps) {
634
634
  }
635
635
  if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url)) {
636
636
  res.setHeader("retry-after", "15");
637
- sendJson(res, 503, { error: "draining", message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" });
637
+ sendJson(res, 503, { error: "draining", errorCode: "draining", message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" });
638
638
  return;
639
639
  }
640
640
  if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url)) {
package/dist/main.js CHANGED
@@ -46,6 +46,7 @@ import { buildSessionAuditPg, buildSessionAuditLocal, buildSessionAudit } from "
46
46
  import { makeDegenerateInstrument } from "./degenerate-instrument.js";
47
47
  import { PlanCacheProbe } from "./plan-cache-probe.js";
48
48
  import { loadSkills } from "./capabilities/skills.js";
49
+ import { applyCenterPlugins } from "./capabilities/center-plugins.js";
49
50
  import { GiteaClient } from "./capabilities/repo-tools.js";
50
51
  import { buildScenarios, selectScenario, centerScenarios, mergeUserSkills, builtinScenarioDetails, centerScenarioDetails, gateScenarioRequest } from "./capabilities/scenarios.js";
51
52
  import { applyLongtailDefer } from "./capabilities/tool-defer.js";
@@ -1270,6 +1271,22 @@ async function main() {
1270
1271
  if (effective?.skills && config.configCenter) {
1271
1272
  skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, process.env.CONFIG_LKG_DISABLED !== "true" ? defaultSkillCacheDir() : undefined);
1272
1273
  }
1274
+ if (effective?.plugins && config.configCenter) {
1275
+ const pluginOut = await applyCenterPlugins(skills, effective, {
1276
+ cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
1277
+ allowHosts: config.pluginsAllowHosts,
1278
+ logger,
1279
+ }).catch((err) => {
1280
+ logger.warn("center_plugins_apply_failed", { error: err instanceof Error ? err.message : String(err) });
1281
+ return undefined;
1282
+ });
1283
+ if (pluginOut) {
1284
+ skills = pluginOut.skills;
1285
+ if (pluginOut.failures.length + pluginOut.letGo.length + pluginOut.lkgUsed.length > 0) {
1286
+ logger.info("center_plugins_applied", { failures: pluginOut.failures, letGo: pluginOut.letGo, lkgUsed: pluginOut.lkgUsed });
1287
+ }
1288
+ }
1289
+ }
1273
1290
  if (bootLkgCandidate && lkgEnabled) {
1274
1291
  const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
1275
1292
  logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
@@ -174,7 +174,7 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
174
174
  closeSync(this.fd);
175
175
  this.fd = undefined;
176
176
  }
177
- const tmp = `${this.ledgerPath}.tmp`;
177
+ const tmp = `${this.ledgerPath}.tmp.${process.pid}`;
178
178
  const lines = [];
179
179
  for (const q of this.bySession.values())
180
180
  for (const entry of q.values())
@@ -133,6 +133,9 @@ export interface EffectiveConfig {
133
133
  commandPolicy?: CommandRule[];
134
134
  approvalRequire?: string[];
135
135
  };
136
+ plugins?: {
137
+ plugins?: unknown[];
138
+ };
136
139
  prompts?: unknown;
137
140
  }
138
141
  export interface ExecutionRuling {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "1.308.0",
3
+ "version": "1.309.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -10,7 +10,11 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "default": "./dist/index.js"
13
- }
13
+ },
14
+ "./main": {
15
+ "default": "./dist/main.js"
16
+ },
17
+ "./package.json": "./package.json"
14
18
  },
15
19
  "files": [
16
20
  "dist",