@wrenai/genbi 0.0.4 → 0.0.5
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/README.md +64 -2
- package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
- package/dist/assets/index-A_Wrq7Ey.css +1 -0
- package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
- package/dist/index.html +2 -2
- package/dist-server/harness/bundle/schema.d.ts +38 -0
- package/dist-server/harness/bundle/schema.js +18 -10
- package/dist-server/harness/bundle/version.js +1 -1
- package/dist-server/harness/cli-args.d.ts +10 -2
- package/dist-server/harness/cli-args.js +24 -8
- package/dist-server/harness/cli.js +75 -9
- package/dist-server/harness/compile/cache.js +4 -4
- package/dist-server/harness/compile/compose-profile.d.ts +51 -14
- package/dist-server/harness/compile/compose-profile.js +85 -19
- package/dist-server/harness/compile/context-loader.d.ts +56 -0
- package/dist-server/harness/compile/context-loader.js +237 -0
- package/dist-server/harness/compile/errors.d.ts +19 -0
- package/dist-server/harness/compile/errors.js +35 -0
- package/dist-server/harness/compile/index.d.ts +4 -3
- package/dist-server/harness/compile/index.js +4 -3
- package/dist-server/harness/compile/pipeline.d.ts +15 -7
- package/dist-server/harness/compile/pipeline.js +62 -14
- package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
- package/dist-server/harness/compile/resolve-binary.js +3 -3
- package/dist-server/harness/compile/types.d.ts +29 -0
- package/dist-server/harness/compile/warble-identity.d.ts +11 -0
- package/dist-server/harness/compile/warble-identity.js +13 -0
- package/dist-server/harness/components/ai-step.d.ts +4 -0
- package/dist-server/harness/components/ai-step.js +40 -0
- package/dist-server/harness/components/broker.d.ts +60 -0
- package/dist-server/harness/components/broker.js +117 -0
- package/dist-server/harness/components/capability-card.d.ts +98 -0
- package/dist-server/harness/components/capability-card.js +169 -0
- package/dist-server/harness/components/context.d.ts +2 -0
- package/dist-server/harness/components/context.js +30 -0
- package/dist-server/harness/components/display.d.ts +6 -0
- package/dist-server/harness/components/display.js +29 -0
- package/dist-server/harness/components/egress-judge.d.ts +12 -0
- package/dist-server/harness/components/egress-judge.js +27 -0
- package/dist-server/harness/components/egress.d.ts +142 -0
- package/dist-server/harness/components/egress.js +469 -0
- package/dist-server/harness/components/in-process.d.ts +7 -0
- package/dist-server/harness/components/in-process.js +245 -0
- package/dist-server/harness/components/normalize.d.ts +35 -0
- package/dist-server/harness/components/normalize.js +245 -0
- package/dist-server/harness/components/plan.d.ts +31 -0
- package/dist-server/harness/components/plan.js +184 -0
- package/dist-server/harness/components/report.d.ts +207 -0
- package/dist-server/harness/components/report.js +309 -0
- package/dist-server/harness/components/runner.d.ts +193 -0
- package/dist-server/harness/components/runner.js +492 -0
- package/dist-server/harness/components/semantic-guards.d.ts +20 -0
- package/dist-server/harness/components/semantic-guards.js +42 -0
- package/dist-server/harness/components/terminal-json.d.ts +18 -0
- package/dist-server/harness/components/terminal-json.js +82 -0
- package/dist-server/harness/components/usage.d.ts +14 -0
- package/dist-server/harness/components/usage.js +19 -0
- package/dist-server/harness/components/wren-access.d.ts +29 -0
- package/dist-server/harness/components/wren-access.js +233 -0
- package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
- package/dist-server/harness/components/zone-dry-run.js +107 -0
- package/dist-server/harness/components/zone-gate.d.ts +76 -0
- package/dist-server/harness/components/zone-gate.js +144 -0
- package/dist-server/harness/events/index.d.ts +1 -1
- package/dist-server/harness/events/types.d.ts +52 -0
- package/dist-server/harness/index.d.ts +9 -2
- package/dist-server/harness/index.js +5 -1
- package/dist-server/harness/loop/executor.d.ts +1 -0
- package/dist-server/harness/loop/executor.js +37 -15
- package/dist-server/harness/npm-package-resolve.d.ts +2 -0
- package/dist-server/harness/npm-package-resolve.js +16 -4
- package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
- package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
- package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
- package/dist-server/harness/providers/adapters/openai.js +6 -0
- package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
- package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
- package/dist-server/harness/providers/binding.d.ts +45 -4
- package/dist-server/harness/providers/binding.js +44 -8
- package/dist-server/harness/providers/index.d.ts +9 -3
- package/dist-server/harness/providers/index.js +10 -3
- package/dist-server/harness/providers/zone.d.ts +96 -0
- package/dist-server/harness/providers/zone.js +112 -0
- package/dist-server/harness/render/answering-query.d.ts +29 -0
- package/dist-server/harness/render/answering-query.js +62 -0
- package/dist-server/harness/render/envelope.d.ts +6 -0
- package/dist-server/harness/render/envelope.js +10 -3
- package/dist-server/harness/route/adapter-spec.d.ts +8 -9
- package/dist-server/harness/route/adapter-spec.js +13 -14
- package/dist-server/harness/route/codex-ask-events.js +2 -1
- package/dist-server/harness/route/codex-ask.d.ts +4 -0
- package/dist-server/harness/route/codex-ask.js +69 -30
- package/dist-server/harness/route/codex-host-query.d.ts +52 -0
- package/dist-server/harness/route/codex-host-query.js +191 -0
- package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
- package/dist-server/harness/route/codex-local-manifest.js +96 -15
- package/dist-server/harness/route/codex-wren-proxy.d.ts +33 -0
- package/dist-server/harness/route/codex-wren-proxy.js +227 -0
- package/dist-server/harness/route/describe.js +45 -6
- package/dist-server/harness/route/in-process.d.ts +22 -1
- package/dist-server/harness/route/in-process.js +54 -4
- package/dist-server/harness/route/route.js +5 -2
- package/dist-server/harness/route/types.d.ts +30 -1
- package/dist-server/harness/session/run.js +30 -10
- package/dist-server/harness/setup/runner.d.ts +2 -0
- package/dist-server/harness/setup/runner.js +38 -17
- package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
- package/dist-server/harness/setup/yaml-scalar.js +62 -0
- package/dist-server/server/adopt.js +1 -17
- package/dist-server/server/app.d.ts +1 -0
- package/dist-server/server/app.js +163 -14
- package/dist-server/server/auth-choice.d.ts +13 -1
- package/dist-server/server/auth-choice.js +66 -1
- package/dist-server/server/bin.js +177 -25
- package/dist-server/server/codex-local-grounding.d.ts +18 -0
- package/dist-server/server/codex-local-grounding.js +92 -0
- package/dist-server/server/codex-native-session.d.ts +25 -0
- package/dist-server/server/codex-native-session.js +1 -0
- package/dist-server/server/codex-runtime-composition.d.ts +34 -0
- package/dist-server/server/codex-runtime-composition.js +250 -0
- package/dist-server/server/codex-structured-ask.d.ts +23 -0
- package/dist-server/server/codex-structured-ask.js +102 -0
- package/dist-server/server/conn-config.js +4 -16
- package/dist-server/server/conversation-socket.d.ts +16 -0
- package/dist-server/server/conversation-socket.js +65 -0
- package/dist-server/server/db.d.ts +46 -0
- package/dist-server/server/db.js +88 -9
- package/dist-server/server/env-detect.d.ts +6 -0
- package/dist-server/server/env-detect.js +7 -0
- package/dist-server/server/fold.d.ts +22 -4
- package/dist-server/server/fold.js +37 -5
- package/dist-server/server/harness.d.ts +2 -2
- package/dist-server/server/harness.js +33 -7
- package/dist-server/server/interactive-terminal.d.ts +10 -6
- package/dist-server/server/interactive-terminal.js +33 -8
- package/dist-server/server/managed-wren-runtime.d.ts +116 -0
- package/dist-server/server/managed-wren-runtime.js +670 -0
- package/dist-server/server/native-artifacts.d.ts +23 -7
- package/dist-server/server/native-artifacts.js +43 -12
- package/dist-server/server/native-component-context.d.ts +19 -0
- package/dist-server/server/native-component-context.js +76 -0
- package/dist-server/server/native-component-preparation.d.ts +20 -0
- package/dist-server/server/native-component-preparation.js +129 -0
- package/dist-server/server/native-component-runtime.d.ts +36 -0
- package/dist-server/server/native-component-runtime.js +92 -0
- package/dist-server/server/native-components.d.ts +84 -0
- package/dist-server/server/native-components.js +160 -0
- package/dist-server/server/native-dispatch-process.d.ts +8 -0
- package/dist-server/server/native-dispatch-process.js +65 -0
- package/dist-server/server/native-runtime-spec.d.ts +89 -0
- package/dist-server/server/native-runtime-spec.js +169 -0
- package/dist-server/server/native-sessions.d.ts +172 -19
- package/dist-server/server/native-sessions.js +839 -244
- package/dist-server/server/native-shutdown.d.ts +6 -0
- package/dist-server/server/native-shutdown.js +28 -0
- package/dist-server/server/native-wren-home.d.ts +18 -0
- package/dist-server/server/native-wren-home.js +262 -0
- package/dist-server/server/node-pty-host.d.ts +2 -1
- package/dist-server/server/node-pty-host.js +3 -2
- package/dist-server/server/profile-registry.d.ts +139 -0
- package/dist-server/server/profile-registry.js +620 -0
- package/dist-server/server/runtime-binding.d.ts +4 -1
- package/dist-server/server/runtime-binding.js +50 -2
- package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
- package/dist-server/server/runtime-host/claude-component-step.js +186 -0
- package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
- package/dist-server/server/runtime-host/codex-app-server.js +239 -0
- package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
- package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
- package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
- package/dist-server/server/runtime-host/codex-component-step.js +308 -0
- package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
- package/dist-server/server/runtime-host/codex-conversation.js +295 -0
- package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
- package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
- package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
- package/dist-server/server/runtime-host/codex-events.js +66 -0
- package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-policy.js +151 -0
- package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
- package/dist-server/server/runtime-host/codex-process.js +96 -0
- package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
- package/dist-server/server/runtime-host/codex-rpc.js +182 -0
- package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
- package/dist-server/server/runtime-host/codex-session.js +492 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
- package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-terminal.js +120 -0
- package/dist-server/server/runtime-host/index.d.ts +5 -0
- package/dist-server/server/runtime-host/index.js +3 -0
- package/dist-server/server/runtime-host/local.d.ts +29 -0
- package/dist-server/server/runtime-host/local.js +90 -0
- package/dist-server/server/runtime-host/policy.d.ts +8 -0
- package/dist-server/server/runtime-host/policy.js +78 -0
- package/dist-server/server/runtime-host/types.d.ts +52 -0
- package/dist-server/server/runtime-host/types.js +62 -0
- package/dist-server/server/structured-query-evidence.d.ts +7 -0
- package/dist-server/server/structured-query-evidence.js +47 -0
- package/dist-server/server/structured-runtime.d.ts +52 -0
- package/dist-server/server/structured-runtime.js +130 -0
- package/dist-server/server/structured-scope.d.ts +4 -0
- package/dist-server/server/structured-scope.js +12 -0
- package/dist-server/server/turn.d.ts +6 -1
- package/dist-server/server/turn.js +82 -25
- package/dist-server/server/wire-types.d.ts +67 -3
- package/dist-server/server/wren-profiles.js +1 -1
- package/managed-wren/README.md +60 -0
- package/managed-wren/dependency-wheels.json +282 -0
- package/managed-wren/manifest.json +373 -0
- package/managed-wren/release-inputs.json +45 -0
- package/managed-wren/runtime-tree.cjs +115 -0
- package/managed-wren/source-inputs.json +40 -0
- package/package.json +15 -6
- package/profiles/genbi-default/context/binding.yml +17 -7
- package/profiles/genbi-default/context/context.json +146 -0
- package/profiles/genbi-default/ir.golden.json +39 -43
- package/profiles/genbi-default/profile.yml +31 -0
- package/profiles/genbi-enrich-context/context/binding.yml +14 -3
- package/profiles/genbi-enrich-context/context/context.json +146 -0
- package/profiles/genbi-enrich-context/ir.golden.json +4 -5
- package/profiles/genbi-monitor/context/binding.yml +13 -5
- package/profiles/genbi-monitor/context/context.json +146 -0
- package/profiles/genbi-monitor/ir.golden.json +13 -7
- package/profiles/genbi-report/context/binding.yml +19 -0
- package/profiles/genbi-report/context/context.json +223 -0
- package/profiles/genbi-report/ir.golden.json +328 -0
- package/profiles/genbi-report/profile.yml +38 -0
- package/profiles/genbi-setup/ir.golden.json +3 -1
- package/dist/assets/index-Xs4q7pNo.css +0 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-owned registry of the Warble profiles a conversation can start inside.
|
|
3
|
+
*
|
|
4
|
+
* Two axes stay separate. The *system* purposes (`setup`, `context_enrichment`) keep their fixed
|
|
5
|
+
* profiles and their host orchestration — see `native-dispatch-registry.ts`. The *conversation*
|
|
6
|
+
* profile is the user-selectable axis: an analysis session starts inside one of the profiles this
|
|
7
|
+
* registry has admitted. A browser only ever names a registry id; the directory, the compiled IR
|
|
8
|
+
* and the launch tuple are resolved here.
|
|
9
|
+
*
|
|
10
|
+
* Admission is the host's own check. Warble validates a profile's structure at dispatch but no
|
|
11
|
+
* longer knows which profiles this product trusts, so a profile the operator adds has to satisfy
|
|
12
|
+
* the same floor the shipped analysis profile does before it can be offered:
|
|
13
|
+
*
|
|
14
|
+
* 1. it compiles with the pinned `warble`, and its `profile:` id is unique and not reserved;
|
|
15
|
+
* 2. every mounted component is analytical, one-shot and side-effect free — the scope
|
|
16
|
+
* `AgentScopeError` (harness/loop/errors.ts) already enforces for the in-process runner;
|
|
17
|
+
* 3. every `required_capabilities` entry is one the shipped analysis profile already requires
|
|
18
|
+
* (the host ceiling is *derived* from that profile's compiled IR, never hand-listed);
|
|
19
|
+
* 4. every step tier is one the persisted runtime configuration can bind (`cheap`, `strong`);
|
|
20
|
+
* 5. the entry form follows from eligibility: two or more native-eligible components, each with
|
|
21
|
+
* an authored `description`, enter at the profile scope; exactly one is pinned; none refuses;
|
|
22
|
+
* 6. the verdict is recomputed whenever the profile's bytes, the warble binary or this host's
|
|
23
|
+
* rule set change, and a profile that stops passing becomes unavailable rather than
|
|
24
|
+
* disappearing.
|
|
25
|
+
*
|
|
26
|
+
* Composition is recorded, not judged. A component whose steps authorize a component-call alias
|
|
27
|
+
* needs a trusted native component host at dispatch; the native CLI targets refuse ANY profile
|
|
28
|
+
* that composes when no host is provided — the shipped analysis profile included, whose dashboard
|
|
29
|
+
* composer is what makes native analysis unavailable until that host is provisioned. Whether the
|
|
30
|
+
* host exists is a runtime fact that native readiness reports per profile; admission only says
|
|
31
|
+
* what the profile is. Each component summary therefore carries its `composes` edges.
|
|
32
|
+
*
|
|
33
|
+
* Tool names (Read, Bash, Write) are a dispatcher materialization and are not in the IR, which is
|
|
34
|
+
* why rule 3 is expressed over capabilities: that is the layer the compiler can vouch for.
|
|
35
|
+
*/
|
|
36
|
+
import { cp, lstat, mkdir, readdir, readFile, realpath, rm } from "node:fs/promises";
|
|
37
|
+
import { existsSync, lstatSync, realpathSync, statSync } from "node:fs";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
import { hashDirectory } from "../harness/compile/fingerprint.js";
|
|
40
|
+
/** The shipped profiles and the role each plays. Anything else under `profiles/` is ignored. */
|
|
41
|
+
export const BUILTIN_PROFILE_ROLES = Object.freeze({
|
|
42
|
+
"genbi-setup": "system",
|
|
43
|
+
"genbi-enrich-context": "system",
|
|
44
|
+
"genbi-default": "conversation",
|
|
45
|
+
"genbi-report": "conversation",
|
|
46
|
+
"genbi-monitor": "conversation",
|
|
47
|
+
});
|
|
48
|
+
/** The profile whose compiled IR defines the host ceiling. */
|
|
49
|
+
export const CEILING_PROFILE_ID = "genbi-default";
|
|
50
|
+
/** The persisted runtime tier rows come from the analysis profile and name exactly these two tiers. */
|
|
51
|
+
export const HOST_TIERS = new Set(["cheap", "strong"]);
|
|
52
|
+
const PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
53
|
+
/**
|
|
54
|
+
* Bump when an admission rule is added, tightened or withdrawn. A stored verdict computed under an
|
|
55
|
+
* older rule set is stale even when the profile bytes and the warble binary are unchanged — the
|
|
56
|
+
* rules are the third input to the verdict. 1: rules 1–6. 2: a pinned entry must not compose.
|
|
57
|
+
* 3: that rule withdrawn (the dispatcher refuses every composing profile without a host, so it
|
|
58
|
+
* never separated anything); composition is recorded on the component summary instead.
|
|
59
|
+
*/
|
|
60
|
+
export const ADMISSION_RULES_VERSION = 3;
|
|
61
|
+
const MAX_REASON_LENGTH = 1200;
|
|
62
|
+
const COMPILE_RETRY_DELAY_MS = 250;
|
|
63
|
+
export class ProfileRegistryError extends Error {
|
|
64
|
+
status;
|
|
65
|
+
constructor(status, message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.status = status;
|
|
68
|
+
this.name = "ProfileRegistryError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function isRecord(value) {
|
|
72
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
function stringAt(record, key) {
|
|
75
|
+
const value = record[key];
|
|
76
|
+
return typeof value === "string" ? value : undefined;
|
|
77
|
+
}
|
|
78
|
+
function nestedKind(record, ...keys) {
|
|
79
|
+
let cursor = record;
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
if (!isRecord(cursor))
|
|
82
|
+
return "";
|
|
83
|
+
cursor = cursor[key];
|
|
84
|
+
}
|
|
85
|
+
return typeof cursor === "string" ? cursor : "";
|
|
86
|
+
}
|
|
87
|
+
function capabilityNames(component) {
|
|
88
|
+
const raw = component["required_capabilities"];
|
|
89
|
+
if (!Array.isArray(raw))
|
|
90
|
+
return [];
|
|
91
|
+
const names = [];
|
|
92
|
+
for (const entry of raw) {
|
|
93
|
+
if (typeof entry === "string")
|
|
94
|
+
names.push(entry);
|
|
95
|
+
else if (isRecord(entry)) {
|
|
96
|
+
const kind = stringAt(entry, "kind") ?? stringAt(entry, "name");
|
|
97
|
+
if (kind !== undefined)
|
|
98
|
+
names.push(kind);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return names;
|
|
102
|
+
}
|
|
103
|
+
/** `alias → component` edges a component's steps authorize; non-empty means the profile composes. */
|
|
104
|
+
function composedAliases(component) {
|
|
105
|
+
const calls = component["llm_calls"];
|
|
106
|
+
if (!Array.isArray(calls))
|
|
107
|
+
return [];
|
|
108
|
+
const edges = [];
|
|
109
|
+
for (const call of calls) {
|
|
110
|
+
if (!isRecord(call) || !Array.isArray(call["component_calls"]))
|
|
111
|
+
continue;
|
|
112
|
+
for (const edge of call["component_calls"]) {
|
|
113
|
+
if (!isRecord(edge))
|
|
114
|
+
continue;
|
|
115
|
+
const alias = stringAt(edge, "alias") ?? "?";
|
|
116
|
+
const target = stringAt(edge, "component") ?? "?";
|
|
117
|
+
edges.push(`${alias} → ${target}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return edges;
|
|
121
|
+
}
|
|
122
|
+
function stepTiers(component) {
|
|
123
|
+
const calls = component["llm_calls"];
|
|
124
|
+
if (!Array.isArray(calls))
|
|
125
|
+
return [];
|
|
126
|
+
const tiers = new Set();
|
|
127
|
+
for (const call of calls) {
|
|
128
|
+
if (isRecord(call)) {
|
|
129
|
+
const tier = stringAt(call, "tier");
|
|
130
|
+
if (tier !== undefined)
|
|
131
|
+
tiers.add(tier);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...tiers].sort();
|
|
135
|
+
}
|
|
136
|
+
function summarizeComponent(component) {
|
|
137
|
+
const type = stringAt(component, "type") ?? "";
|
|
138
|
+
const trigger = nestedKind(component, "trigger", "kind");
|
|
139
|
+
const outcome = nestedKind(component, "effect", "outcome", "kind");
|
|
140
|
+
const realizationKind = stringAt(component, "realization_kind") ?? "";
|
|
141
|
+
const entrypoint = component["entrypoint"] !== false;
|
|
142
|
+
const description = stringAt(component, "description");
|
|
143
|
+
const hostScoped = type === "analytical" && trigger === "one_shot" && outcome === "none";
|
|
144
|
+
return {
|
|
145
|
+
id: stringAt(component, "id") ?? "<unnamed>",
|
|
146
|
+
type,
|
|
147
|
+
trigger,
|
|
148
|
+
outcome,
|
|
149
|
+
realizationKind,
|
|
150
|
+
entrypoint,
|
|
151
|
+
tiers: stepTiers(component),
|
|
152
|
+
capabilities: capabilityNames(component),
|
|
153
|
+
hasDescription: description !== undefined && description.trim().length > 0,
|
|
154
|
+
// Mirrors what the dispatcher accepts as a native entry: a one-shot skill with no outcome that
|
|
155
|
+
// a session may start directly. `entrypoint: false` marks a callee-only component.
|
|
156
|
+
nativeEligible: hostScoped && realizationKind === "skill" && entrypoint,
|
|
157
|
+
composes: composedAliases(component),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function componentsOf(ir) {
|
|
161
|
+
if (!isRecord(ir) || !Array.isArray(ir["components"]))
|
|
162
|
+
return undefined;
|
|
163
|
+
return ir["components"].filter(isRecord);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The capability ceiling is whatever the shipped analysis profile already requires, read from its
|
|
167
|
+
* compiled IR. Listing the names by hand would let the ceiling drift from the profile it describes.
|
|
168
|
+
*/
|
|
169
|
+
export function deriveHostCeiling(ceilingIr) {
|
|
170
|
+
const components = componentsOf(ceilingIr);
|
|
171
|
+
if (components === undefined || components.length === 0) {
|
|
172
|
+
throw new Error(`host ceiling profile "${CEILING_PROFILE_ID}" compiled to an IR with no components`);
|
|
173
|
+
}
|
|
174
|
+
const capabilities = new Set();
|
|
175
|
+
for (const component of components)
|
|
176
|
+
for (const name of capabilityNames(component))
|
|
177
|
+
capabilities.add(name);
|
|
178
|
+
return { capabilities, tiers: HOST_TIERS };
|
|
179
|
+
}
|
|
180
|
+
/** Rules 2–5 over a compiled IR. Pure: rule 1 (compiling) and rule 6 (staleness) live in the registry. */
|
|
181
|
+
export function admitCompiledProfile(ir, options) {
|
|
182
|
+
const reasons = [];
|
|
183
|
+
const components = componentsOf(ir);
|
|
184
|
+
if (!isRecord(ir) || components === undefined) {
|
|
185
|
+
return { status: "unavailable", reasons: ["compiled IR is not an object with a components array"], components: [] };
|
|
186
|
+
}
|
|
187
|
+
const irVersion = stringAt(ir, "warble_ir_version");
|
|
188
|
+
const compiledId = stringAt(ir, "profile");
|
|
189
|
+
if (compiledId !== options.expectedId) {
|
|
190
|
+
reasons.push(`compiled profile id "${compiledId ?? ""}" does not match the registered id "${options.expectedId}"`);
|
|
191
|
+
}
|
|
192
|
+
if (components.length === 0)
|
|
193
|
+
reasons.push("profile mounts no components");
|
|
194
|
+
const summaries = components.map(summarizeComponent);
|
|
195
|
+
const outOfScope = summaries.filter((c) => !(c.type === "analytical" && c.trigger === "one_shot" && c.outcome === "none"));
|
|
196
|
+
for (const c of outOfScope) {
|
|
197
|
+
reasons.push(`component "${c.id}" is outside the host's execution scope: type="${c.type}", trigger="${c.trigger}", outcome="${c.outcome}" ` +
|
|
198
|
+
`(a conversation profile may only mount type="analytical", trigger="one_shot", outcome="none")`);
|
|
199
|
+
}
|
|
200
|
+
for (const c of summaries) {
|
|
201
|
+
const excess = c.capabilities.filter((name) => !options.ceiling.capabilities.has(name));
|
|
202
|
+
if (excess.length > 0) {
|
|
203
|
+
reasons.push(`component "${c.id}" requires capabilities outside the host ceiling: ${excess.join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const c of summaries) {
|
|
207
|
+
const foreignTiers = c.tiers.filter((tier) => !options.ceiling.tiers.has(tier));
|
|
208
|
+
if (foreignTiers.length > 0) {
|
|
209
|
+
reasons.push(`component "${c.id}" uses step tiers the runtime configuration cannot bind: ${foreignTiers.join(", ")} (allowed: ${[...options.ceiling.tiers].join(", ")})`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const eligible = summaries.filter((c) => c.nativeEligible);
|
|
213
|
+
let entry;
|
|
214
|
+
if (eligible.length === 0) {
|
|
215
|
+
reasons.push("profile has no native-eligible component (a one-shot skill with no outcome that a session may start)");
|
|
216
|
+
}
|
|
217
|
+
else if (eligible.length === 1) {
|
|
218
|
+
entry = { kind: "agent", verb: eligible[0].id };
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const undescribed = eligible.filter((c) => !c.hasDescription).map((c) => c.id);
|
|
222
|
+
if (undescribed.length > 0) {
|
|
223
|
+
reasons.push(`scope entry requires an authored description on every native-eligible component; missing on: ${undescribed.join(", ")}`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
entry = { kind: "scope" };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
status: reasons.length === 0 ? "admitted" : "unavailable",
|
|
231
|
+
reasons,
|
|
232
|
+
...(reasons.length === 0 && entry !== undefined ? { entry } : {}),
|
|
233
|
+
...(irVersion !== undefined ? { irVersion } : {}),
|
|
234
|
+
components: summaries,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Source directories
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/** The `profile:` scalar of a profile.yml. Flat YAML, read the same way `composeUserProfile` does. */
|
|
241
|
+
export function readProfileId(profileYamlText) {
|
|
242
|
+
const matches = [...profileYamlText.matchAll(/^profile:[ \t]*["']?([^"'\s#]+)["']?[ \t]*(?:#.*)?$/gm)];
|
|
243
|
+
if (matches.length !== 1)
|
|
244
|
+
return undefined;
|
|
245
|
+
return matches[0][1];
|
|
246
|
+
}
|
|
247
|
+
function isInside(candidate, root) {
|
|
248
|
+
const relative = path.relative(root, candidate);
|
|
249
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* A user profile is handed to the server as a directory path on this machine, the same way the
|
|
253
|
+
* adopt flow receives a project path. The path is checked, not trusted: it must be absolute, be a
|
|
254
|
+
* real directory rather than a link, hold a `profile.yml`, and sit outside the roots the registry
|
|
255
|
+
* itself owns, so a request can never register the package tree or the user-data tree as "new".
|
|
256
|
+
*/
|
|
257
|
+
export function validateUserProfileSourcePath(sourcePath, options) {
|
|
258
|
+
if (typeof sourcePath !== "string" || sourcePath.trim().length === 0)
|
|
259
|
+
return { ok: false, message: "sourcePath is required" };
|
|
260
|
+
const trimmed = sourcePath.trim();
|
|
261
|
+
if (!path.isAbsolute(trimmed))
|
|
262
|
+
return { ok: false, message: `sourcePath must be an absolute path: "${trimmed}"` };
|
|
263
|
+
const resolved = path.resolve(trimmed);
|
|
264
|
+
if (!existsSync(resolved))
|
|
265
|
+
return { ok: false, message: `no such directory: "${resolved}"` };
|
|
266
|
+
if (lstatSync(resolved).isSymbolicLink())
|
|
267
|
+
return { ok: false, message: `sourcePath must not be a symbolic link: "${resolved}"` };
|
|
268
|
+
if (!statSync(resolved).isDirectory())
|
|
269
|
+
return { ok: false, message: `not a directory: "${resolved}"` };
|
|
270
|
+
const profileYaml = path.join(resolved, "profile.yml");
|
|
271
|
+
if (!existsSync(profileYaml) || !statSync(profileYaml).isFile()) {
|
|
272
|
+
return { ok: false, message: `"${resolved}" is not a Warble profile (no profile.yml)` };
|
|
273
|
+
}
|
|
274
|
+
const real = realpathSync(resolved);
|
|
275
|
+
for (const root of options.forbiddenRoots) {
|
|
276
|
+
let realRoot;
|
|
277
|
+
try {
|
|
278
|
+
realRoot = realpathSync(root);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
realRoot = path.resolve(root);
|
|
282
|
+
}
|
|
283
|
+
if (isInside(real, realRoot)) {
|
|
284
|
+
return { ok: false, message: `sourcePath must be outside the registry's own directories: "${resolved}" is under "${root}"` };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { ok: true, resolved, real };
|
|
288
|
+
}
|
|
289
|
+
/** Rejects a tree with any symbolic link in it: the copy would otherwise follow it out of the directory. */
|
|
290
|
+
async function assertNoSymlinks(root) {
|
|
291
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
const full = path.join(root, entry.name);
|
|
294
|
+
const info = await lstat(full);
|
|
295
|
+
if (info.isSymbolicLink())
|
|
296
|
+
throw new ProfileRegistryError(400, `profile directory must not contain symbolic links: "${full}"`);
|
|
297
|
+
if (info.isDirectory())
|
|
298
|
+
await assertNoSymlinks(full);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function truncateReason(message) {
|
|
302
|
+
const oneLine = message.replace(/\s+/g, " ").trim();
|
|
303
|
+
return oneLine.length > MAX_REASON_LENGTH ? `${oneLine.slice(0, MAX_REASON_LENGTH - 1)}…` : oneLine;
|
|
304
|
+
}
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Wire projection
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
export function toWarbleProfileDto(row) {
|
|
309
|
+
let components = [];
|
|
310
|
+
try {
|
|
311
|
+
const parsed = JSON.parse(row.componentsJson);
|
|
312
|
+
if (Array.isArray(parsed))
|
|
313
|
+
components = parsed.filter(isRecord);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
components = [];
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
id: row.id,
|
|
320
|
+
kind: row.kind,
|
|
321
|
+
role: row.role,
|
|
322
|
+
selectable: row.role === "conversation" && row.admissionStatus === "admitted",
|
|
323
|
+
admission: {
|
|
324
|
+
status: row.admissionStatus,
|
|
325
|
+
...(row.admissionReason !== null ? { reason: row.admissionReason } : {}),
|
|
326
|
+
checkedAt: row.updatedAt,
|
|
327
|
+
},
|
|
328
|
+
...(row.entryKind !== null ? { entry: { kind: row.entryKind, ...(row.entryVerb !== null ? { verb: row.entryVerb } : {}) } } : {}),
|
|
329
|
+
...(row.irVersion !== null ? { irVersion: row.irVersion } : {}),
|
|
330
|
+
components: components.map((c) => ({
|
|
331
|
+
id: c.id,
|
|
332
|
+
type: c.type,
|
|
333
|
+
nativeEligible: c.nativeEligible,
|
|
334
|
+
hasDescription: c.hasDescription,
|
|
335
|
+
tiers: c.tiers,
|
|
336
|
+
capabilities: c.capabilities,
|
|
337
|
+
composes: Array.isArray(c.composes) ? c.composes : [],
|
|
338
|
+
})),
|
|
339
|
+
...(row.kind === "user" ? { sourceDir: row.sourceDir } : {}),
|
|
340
|
+
createdAt: row.createdAt,
|
|
341
|
+
updatedAt: row.updatedAt,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export class ProfileRegistry {
|
|
345
|
+
options;
|
|
346
|
+
ready;
|
|
347
|
+
ceiling;
|
|
348
|
+
ceilingFailure;
|
|
349
|
+
now;
|
|
350
|
+
constructor(options) {
|
|
351
|
+
this.options = options;
|
|
352
|
+
this.now = options.now ?? (() => new Date());
|
|
353
|
+
}
|
|
354
|
+
/** Registers the shipped profiles once and re-checks stored rows whose inputs changed. Idempotent. */
|
|
355
|
+
ensureReady() {
|
|
356
|
+
this.ready ??= this.initialize().catch((error) => {
|
|
357
|
+
// Let a later call retry rather than pinning the failure for the process lifetime.
|
|
358
|
+
this.ready = undefined;
|
|
359
|
+
throw error;
|
|
360
|
+
});
|
|
361
|
+
return this.ready;
|
|
362
|
+
}
|
|
363
|
+
async initialize() {
|
|
364
|
+
await this.registerBuiltins();
|
|
365
|
+
for (const row of this.options.store.listWarbleProfiles()) {
|
|
366
|
+
if (row.kind !== "user")
|
|
367
|
+
continue;
|
|
368
|
+
try {
|
|
369
|
+
await this.readmitIfStale(row);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
// One unreadable user directory must not take the whole registry down with it.
|
|
373
|
+
this.options.store.upsertWarbleProfile({
|
|
374
|
+
...row,
|
|
375
|
+
admissionStatus: "unavailable",
|
|
376
|
+
admissionReason: truncateReason(`re-admission failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
377
|
+
entryKind: null,
|
|
378
|
+
entryVerb: null,
|
|
379
|
+
admittedAt: null,
|
|
380
|
+
rulesVersion: ADMISSION_RULES_VERSION,
|
|
381
|
+
updatedAt: this.now().toISOString(),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async registerBuiltins() {
|
|
387
|
+
// The ceiling profile is admitted first: every other conversation profile is judged against it.
|
|
388
|
+
const ceilingDir = path.join(this.options.builtinProfilesDir, CEILING_PROFILE_ID);
|
|
389
|
+
if (!existsSync(ceilingDir)) {
|
|
390
|
+
throw new Error(`built-in profile "${CEILING_PROFILE_ID}" is missing from ${this.options.builtinProfilesDir}`);
|
|
391
|
+
}
|
|
392
|
+
await this.registerBuiltin(CEILING_PROFILE_ID, "conversation", ceilingDir);
|
|
393
|
+
for (const [id, role] of Object.entries(BUILTIN_PROFILE_ROLES)) {
|
|
394
|
+
if (id === CEILING_PROFILE_ID)
|
|
395
|
+
continue;
|
|
396
|
+
const dir = path.join(this.options.builtinProfilesDir, id);
|
|
397
|
+
if (existsSync(dir))
|
|
398
|
+
await this.registerBuiltin(id, role, dir);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async registerBuiltin(id, role, dir) {
|
|
402
|
+
const existing = this.options.store.getWarbleProfile(id);
|
|
403
|
+
if (existing !== undefined && existing.kind !== "builtin") {
|
|
404
|
+
// A user row cannot shadow a shipped id; the shipped one wins and the row is corrected.
|
|
405
|
+
this.options.store.deleteWarbleProfile(id);
|
|
406
|
+
}
|
|
407
|
+
await this.admitAndStore({ id, kind: "builtin", role, sourceDir: dir, ...(existing !== undefined ? { createdAt: existing.createdAt } : {}) });
|
|
408
|
+
}
|
|
409
|
+
async readmitIfStale(row) {
|
|
410
|
+
if (!existsSync(row.sourceDir)) {
|
|
411
|
+
this.options.store.upsertWarbleProfile({
|
|
412
|
+
...row,
|
|
413
|
+
admissionStatus: "unavailable",
|
|
414
|
+
admissionReason: `profile directory is missing: "${row.sourceDir}"`,
|
|
415
|
+
entryKind: null,
|
|
416
|
+
entryVerb: null,
|
|
417
|
+
admittedAt: null,
|
|
418
|
+
rulesVersion: ADMISSION_RULES_VERSION,
|
|
419
|
+
updatedAt: this.now().toISOString(),
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const [profileHash, warbleIdentity] = await Promise.all([hashDirectory(row.sourceDir), this.options.warbleIdentity()]);
|
|
424
|
+
// A stored verdict is reused only when it was a pass and nothing it depended on has moved: the
|
|
425
|
+
// profile bytes, the warble binary, and this host's rule set. An `unavailable` row is always
|
|
426
|
+
// re-checked: its reason may have been the environment (a compile that collided, a ceiling that
|
|
427
|
+
// was down at that boot), and the only way to find out is to run admission again. A genuinely
|
|
428
|
+
// refused profile costs one compile per boot and stays refused.
|
|
429
|
+
if (row.admissionStatus === "admitted" && row.rulesVersion === ADMISSION_RULES_VERSION && profileHash === row.profileHash && warbleIdentity === row.warbleIdentity && this.ceilingFailure === undefined)
|
|
430
|
+
return;
|
|
431
|
+
await this.admitAndStore({ id: row.id, kind: row.kind, role: row.role, sourceDir: row.sourceDir, createdAt: row.createdAt });
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Runs rules 1–5 for one directory and persists the verdict. The ceiling profile's own verdict
|
|
435
|
+
* also fixes the ceiling; while it fails to compile no conversation profile can be admitted,
|
|
436
|
+
* and each row says so rather than silently inheriting an empty ceiling.
|
|
437
|
+
*/
|
|
438
|
+
async admitAndStore(input) {
|
|
439
|
+
const now = this.now().toISOString();
|
|
440
|
+
const [profileHash, warbleIdentity] = await Promise.all([hashDirectory(input.sourceDir), this.options.warbleIdentity()]);
|
|
441
|
+
const base = {
|
|
442
|
+
id: input.id,
|
|
443
|
+
kind: input.kind,
|
|
444
|
+
role: input.role,
|
|
445
|
+
sourceDir: input.sourceDir,
|
|
446
|
+
profileHash,
|
|
447
|
+
warbleIdentity,
|
|
448
|
+
rulesVersion: ADMISSION_RULES_VERSION,
|
|
449
|
+
createdAt: input.createdAt ?? now,
|
|
450
|
+
updatedAt: now,
|
|
451
|
+
};
|
|
452
|
+
const unavailable = (reason) => this.options.store.upsertWarbleProfile({
|
|
453
|
+
...base,
|
|
454
|
+
admissionStatus: "unavailable",
|
|
455
|
+
admissionReason: reason,
|
|
456
|
+
entryKind: null,
|
|
457
|
+
entryVerb: null,
|
|
458
|
+
irVersion: null,
|
|
459
|
+
componentsJson: "[]",
|
|
460
|
+
admittedAt: null,
|
|
461
|
+
});
|
|
462
|
+
if (input.role === "system") {
|
|
463
|
+
// System purposes are dispatched by their host purpose and never offered to a browser, so the
|
|
464
|
+
// conversation floor does not apply. The row exists so the registry lists every shipped profile.
|
|
465
|
+
return this.options.store.upsertWarbleProfile({
|
|
466
|
+
...base,
|
|
467
|
+
admissionStatus: "admitted",
|
|
468
|
+
admissionReason: "system purpose profile: dispatched by its host purpose, never selectable for a conversation",
|
|
469
|
+
entryKind: null,
|
|
470
|
+
entryVerb: null,
|
|
471
|
+
irVersion: null,
|
|
472
|
+
componentsJson: "[]",
|
|
473
|
+
admittedAt: now,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
let ir;
|
|
477
|
+
try {
|
|
478
|
+
const compiled = await this.compileOnceWithRetry(input.sourceDir);
|
|
479
|
+
ir = JSON.parse(await readFile(compiled.irPath, "utf-8"));
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
const reason = truncateReason(`warble compile failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
483
|
+
if (input.id === CEILING_PROFILE_ID) {
|
|
484
|
+
this.ceiling = undefined;
|
|
485
|
+
this.ceilingFailure = reason;
|
|
486
|
+
}
|
|
487
|
+
return unavailable(reason);
|
|
488
|
+
}
|
|
489
|
+
if (input.id === CEILING_PROFILE_ID) {
|
|
490
|
+
try {
|
|
491
|
+
this.ceiling = deriveHostCeiling(ir);
|
|
492
|
+
this.ceilingFailure = undefined;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
this.ceiling = undefined;
|
|
496
|
+
this.ceilingFailure = truncateReason(error instanceof Error ? error.message : String(error));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (this.ceiling === undefined) {
|
|
500
|
+
return unavailable(`host ceiling is unavailable: ${this.ceilingFailure ?? `"${CEILING_PROFILE_ID}" has not been admitted`}`);
|
|
501
|
+
}
|
|
502
|
+
const verdict = admitCompiledProfile(ir, { expectedId: input.id, ceiling: this.ceiling });
|
|
503
|
+
return this.options.store.upsertWarbleProfile({
|
|
504
|
+
...base,
|
|
505
|
+
admissionStatus: verdict.status,
|
|
506
|
+
admissionReason: verdict.reasons.length === 0 ? null : truncateReason(verdict.reasons.join("; ")),
|
|
507
|
+
entryKind: verdict.entry?.kind ?? null,
|
|
508
|
+
entryVerb: verdict.entry?.verb ?? null,
|
|
509
|
+
irVersion: verdict.irVersion ?? null,
|
|
510
|
+
componentsJson: JSON.stringify(verdict.components),
|
|
511
|
+
admittedAt: verdict.status === "admitted" ? now : null,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* `warble compile` re-derives its fetched Hub cache on every run and documents that two
|
|
516
|
+
* concurrent runs can collide on the rename, failing one of them loudly and transiently. A BFF
|
|
517
|
+
* compiles for readiness and sessions while this registry is admitting, so one bounded retry
|
|
518
|
+
* keeps a shipped profile from being recorded as unavailable over that collision. A profile that
|
|
519
|
+
* fails twice is recorded with the second failure.
|
|
520
|
+
*/
|
|
521
|
+
async compileOnceWithRetry(sourceDir) {
|
|
522
|
+
try {
|
|
523
|
+
return await this.options.compileRaw(sourceDir);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
await new Promise((resolve) => setTimeout(resolve, COMPILE_RETRY_DELAY_MS));
|
|
527
|
+
return this.options.compileRaw(sourceDir);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async list() {
|
|
531
|
+
await this.ensureReady();
|
|
532
|
+
return this.options.store.listWarbleProfiles();
|
|
533
|
+
}
|
|
534
|
+
async get(id) {
|
|
535
|
+
await this.ensureReady();
|
|
536
|
+
return this.options.store.getWarbleProfile(id);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Copies a profile directory from `sourcePath` into the user-data tree and admits it. The copy is
|
|
540
|
+
* what the registry owns from then on; the original is never read again.
|
|
541
|
+
*/
|
|
542
|
+
async add(sourcePath) {
|
|
543
|
+
await this.ensureReady();
|
|
544
|
+
const check = validateUserProfileSourcePath(sourcePath, {
|
|
545
|
+
forbiddenRoots: [this.options.builtinProfilesDir, this.options.userProfilesDir],
|
|
546
|
+
});
|
|
547
|
+
if (!check.ok)
|
|
548
|
+
throw new ProfileRegistryError(400, check.message);
|
|
549
|
+
await assertNoSymlinks(check.real);
|
|
550
|
+
const profileYaml = await readFile(path.join(check.real, "profile.yml"), "utf-8");
|
|
551
|
+
const id = readProfileId(profileYaml);
|
|
552
|
+
if (id === undefined)
|
|
553
|
+
throw new ProfileRegistryError(400, `profile.yml at "${check.resolved}" must declare exactly one top-level "profile:" id`);
|
|
554
|
+
if (!PROFILE_ID_PATTERN.test(id)) {
|
|
555
|
+
throw new ProfileRegistryError(400, `profile id "${id}" is invalid: use lowercase letters, digits, "-" or "_" (max 64 characters)`);
|
|
556
|
+
}
|
|
557
|
+
if (Object.hasOwn(BUILTIN_PROFILE_ROLES, id))
|
|
558
|
+
throw new ProfileRegistryError(409, `profile id "${id}" is reserved for a built-in profile`);
|
|
559
|
+
if (this.options.store.getWarbleProfile(id) !== undefined)
|
|
560
|
+
throw new ProfileRegistryError(409, `a profile with id "${id}" is already registered`);
|
|
561
|
+
const destination = path.join(this.options.userProfilesDir, id);
|
|
562
|
+
await mkdir(this.options.userProfilesDir, { recursive: true });
|
|
563
|
+
// Claim the destination atomically: a non-recursive mkdir fails with EEXIST for a second
|
|
564
|
+
// concurrent add of the same id, or for a directory left behind by an earlier failure, so two
|
|
565
|
+
// sources can never be merged into one copy.
|
|
566
|
+
try {
|
|
567
|
+
await mkdir(destination);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
if (error.code === "EEXIST") {
|
|
571
|
+
throw new ProfileRegistryError(409, `"${destination}" already exists; remove it before registering "${id}" again`);
|
|
572
|
+
}
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
await cp(check.real, destination, { recursive: true, dereference: false, force: false });
|
|
577
|
+
return await this.admitAndStore({ id, kind: "user", role: "conversation", sourceDir: destination });
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
// Nothing half-copied survives a failure: the next add of this id starts from a clean claim.
|
|
581
|
+
await rm(destination, { recursive: true, force: true });
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/** Removes a user profile and its copy. A profile any native session has run inside stays. */
|
|
586
|
+
async remove(id) {
|
|
587
|
+
await this.ensureReady();
|
|
588
|
+
const row = this.options.store.getWarbleProfile(id);
|
|
589
|
+
if (row === undefined)
|
|
590
|
+
throw new ProfileRegistryError(404, `no profile with id "${id}"`);
|
|
591
|
+
if (row.kind !== "user")
|
|
592
|
+
throw new ProfileRegistryError(409, `profile "${id}" is built in and cannot be removed`);
|
|
593
|
+
const referenced = this.options.store.countNativeSessionsByDispatchProfile(id);
|
|
594
|
+
if (referenced > 0) {
|
|
595
|
+
throw new ProfileRegistryError(409, `profile "${id}" is referenced by ${referenced} native session${referenced === 1 ? "" : "s"} and cannot be removed while that history exists`);
|
|
596
|
+
}
|
|
597
|
+
this.options.store.deleteWarbleProfile(id);
|
|
598
|
+
const userRoot = await realpath(this.options.userProfilesDir).catch(() => path.resolve(this.options.userProfilesDir));
|
|
599
|
+
const target = await realpath(row.sourceDir).catch(() => undefined);
|
|
600
|
+
if (target !== undefined && isInside(target, userRoot) && target !== userRoot) {
|
|
601
|
+
await rm(target, { recursive: true, force: true });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* The directory a conversation session compiles from, for an id the browser selected. Refuses
|
|
606
|
+
* anything that is not an admitted conversation profile, with the stored reason.
|
|
607
|
+
*/
|
|
608
|
+
async resolveConversationProfileSource(id) {
|
|
609
|
+
await this.ensureReady();
|
|
610
|
+
const row = this.options.store.getWarbleProfile(id);
|
|
611
|
+
if (row === undefined)
|
|
612
|
+
throw new ProfileRegistryError(404, `no profile with id "${id}"`);
|
|
613
|
+
if (row.role !== "conversation")
|
|
614
|
+
throw new ProfileRegistryError(409, `profile "${id}" is a system purpose profile and cannot start a conversation`);
|
|
615
|
+
if (row.admissionStatus !== "admitted") {
|
|
616
|
+
throw new ProfileRegistryError(409, `profile "${id}" is unavailable: ${row.admissionReason ?? "admission failed"}`);
|
|
617
|
+
}
|
|
618
|
+
return { sourceDir: row.sourceDir, row };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
@@ -10,7 +10,10 @@ export declare class RuntimeBindingError extends Error {
|
|
|
10
10
|
* before a user turn reaches the dispatcher.
|
|
11
11
|
*/
|
|
12
12
|
export declare const CLAUDE_AGENT_SDK_PER_STEP_MODELS: readonly ["sonnet", "opus", "haiku", "inherit"];
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The old single Model field is the default for every tier without a model
|
|
15
|
+
* override; in gateway mode the gateway's own default model comes first.
|
|
16
|
+
*/
|
|
14
17
|
export declare function effectiveTierModel(binding: TierModelBinding, settings: RuntimeSettings): string | undefined;
|
|
15
18
|
/**
|
|
16
19
|
* Returns a browser-safe correction for an already persisted Runtime. This is
|