@open-everywhere/create 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +69 -0
- package/bin/create-open-everywhere.js +57 -0
- package/package.json +57 -0
- package/src/cli.ts +241 -0
- package/src/deploy-probe.ts +99 -0
- package/src/engine.ts +106 -0
- package/src/env-file.ts +53 -0
- package/src/scaffold.ts +188 -0
- package/src/steps/build-deploy.ts +78 -0
- package/src/steps/database.ts +85 -0
- package/src/steps/env-checklist.ts +55 -0
- package/src/steps/index.ts +49 -0
- package/src/steps/prerequisites.ts +50 -0
- package/src/steps/repo.ts +69 -0
- package/src/steps/roster.ts +111 -0
- package/src/steps/slack.ts +74 -0
- package/src/steps/vercel.ts +40 -0
- package/src/steps/verify.ts +119 -0
- package/src/version.ts +20 -0
- package/template/.env.example +58 -0
- package/template/.mcp.json +8 -0
- package/template/AGENTS.md +62 -0
- package/template/CLAUDE.md +1 -0
- package/template/README.md +87 -0
- package/template/agent/access/access-requests.ts +69 -0
- package/template/agent/access/browser-hosts.ts +28 -0
- package/template/agent/access/bundles.ts +78 -0
- package/template/agent/access/connector-credentials.ts +74 -0
- package/template/agent/access/credential-bindings.generated.ts +17 -0
- package/template/agent/access/credential-bindings.ts +28 -0
- package/template/agent/access/grants.generated.ts +18 -0
- package/template/agent/access/grants.ts +30 -0
- package/template/agent/access/network-access.ts +32 -0
- package/template/agent/agent.ts +125 -0
- package/template/agent/blob/hosting.ts +42 -0
- package/template/agent/identity/authors.ts +79 -0
- package/template/agent/identity/groups-overlay.ts +24 -0
- package/template/agent/identity/groups.generated.ts +17 -0
- package/template/agent/identity/groups.ts +84 -0
- package/template/agent/identity/principals.ts +37 -0
- package/template/agent/identity/surfaces.ts +48 -0
- package/template/agent/instructions/access.ts +11 -0
- package/template/agent/instructions.md +295 -0
- package/template/agent/instrumentation.ts +6 -0
- package/template/agent/lib/memory-grants.ts +52 -0
- package/template/agent/lib/register-adapters.ts +286 -0
- package/template/agent/lib/register-config.ts +185 -0
- package/template/agent/memory/config/agents-overlay.ts +26 -0
- package/template/agent/memory/config/agents.generated.ts +21 -0
- package/template/agent/memory/config/domains/dm.ts +10 -0
- package/template/agent/memory/config/domains/index.ts +125 -0
- package/template/agent/memory/config/domains/org.ts +27 -0
- package/template/agent/memory/config/domains.ts +165 -0
- package/template/agent/model/models.ts +60 -0
- package/template/agent/model/policy.ts +19 -0
- package/template/agent/model/stacks.ts +75 -0
- package/template/agent/plugins/generated/index.ts +13 -0
- package/template/agent/plugins/registry.ts +16 -0
- package/template/agent/plugins/skill-content.ts +13 -0
- package/template/agent/roles/roles.ts +146 -0
- package/template/agent/routines/collectors.ts +89 -0
- package/template/agent/sandbox/backend-select.ts +16 -0
- package/template/agent/sandbox/sandbox.ts +242 -0
- package/template/agent/sandbox/workspace/.keep +6 -0
- package/template/agent/settings/settings.ts +19 -0
- package/template/agent/spend/budgets.ts +25 -0
- package/template/agent/spend/checkpoints.ts +51 -0
- package/template/agent/spend/model-pricing.ts +25 -0
- package/template/agent/webhooks/hooks-overlay.ts +34 -0
- package/template/agent/webhooks/hooks.generated.ts +17 -0
- package/template/agent/webhooks/hooks.ts +133 -0
- package/template/claude/skills/quickstart-open-everywhere/SKILL.md +167 -0
- package/template/claude/skills/upgrade-open-everywhere/SKILL.md +160 -0
- package/template/gitignore +42 -0
- package/template/package.json +62 -0
- package/template/roster.config.ts +39 -0
- package/template/roster.lock.json +182 -0
- package/template/scripts/env-pipe.mjs +74 -0
- package/template/scripts/ops-mcp.ts +35 -0
- package/template/tsconfig.json +23 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// App → core wiring. Registers the deployer-owned config (bundles, bindings, the plugin
|
|
2
|
+
// registry, skill content, memory domains, role presets, webhook triggers, spend budgets,
|
|
3
|
+
// the model-policy envelope) into core's config registry (access/config.ts)
|
|
4
|
+
// as a startup side effect — so core modules resolve access WITHOUT importing app data
|
|
5
|
+
// (the dependency-inversion seam for the planned core/app split; docs/core-app-separation.md in the Open Everywhere repository).
|
|
6
|
+
//
|
|
7
|
+
// Imported for its side effect by agent.ts (the agent definition, loaded at boot before any
|
|
8
|
+
// hook/tool/connection handler runs) and by tests that exercise the resolver or its consumers.
|
|
9
|
+
// Lives in agent/lib/ — Eve's import-only directory (never mounted, not a discovered primitive).
|
|
10
|
+
// No consumer calls getConfig() at module top level, so this registration always completes
|
|
11
|
+
// before the first resolve.
|
|
12
|
+
|
|
13
|
+
import { registerConfig } from "@open-everywhere/core/agent/access/config.js";
|
|
14
|
+
import { registerSettings } from "@open-everywhere/core/agent/settings/settings.js";
|
|
15
|
+
import { registerModelPricing } from "@open-everywhere/core/agent/spend/pricing.js";
|
|
16
|
+
import { loadModelPricing } from "../spend/model-pricing.js";
|
|
17
|
+
import { SETTINGS_OVERRIDES } from "../settings/settings.js";
|
|
18
|
+
import { setRegisteredConnectorCredentials } from "@open-everywhere/core/agent/access/connector-credentials.js";
|
|
19
|
+
import { assertRoutesWithinPolicy } from "@open-everywhere/core/agent/model/policy.js";
|
|
20
|
+
import { assertDomainCredentialsResolvable } from "@open-everywhere/core/agent/access/domain-credentials.js";
|
|
21
|
+
import {
|
|
22
|
+
assertWindowSourcesVouched,
|
|
23
|
+
assertGovernanceDomains,
|
|
24
|
+
assertComplianceDomains,
|
|
25
|
+
assertReportChannels,
|
|
26
|
+
warnSilentRoomGrainWithoutPublicSignal,
|
|
27
|
+
} from "@open-everywhere/core/agent/memory/domain.js";
|
|
28
|
+
import { isVouchedInternal, declaredTrustForKeys } from "@open-everywhere/core/agent/identity/trust.js";
|
|
29
|
+
import { resolveExternalPolicy } from "@open-everywhere/core/agent/access/external-policy.js";
|
|
30
|
+
import { loadBundles, loadBindings } from "../access/bundles.js";
|
|
31
|
+
import { loadNetworkAccess } from "../access/network-access.js";
|
|
32
|
+
import { loadConnectorCredentials } from "../access/connector-credentials.js";
|
|
33
|
+
import { loadAccessRequests } from "../access/access-requests.js";
|
|
34
|
+
import { PLUGIN_REGISTRY } from "../plugins/registry.js";
|
|
35
|
+
import { SKILL_CONTENT } from "../plugins/skill-content.js";
|
|
36
|
+
import { loadDomains, loadSilos, domainsForSurface } from "../memory/config/domains.js";
|
|
37
|
+
import { loadRoles } from "../roles/roles.js";
|
|
38
|
+
import { loadWebhooks } from "../webhooks/hooks.js";
|
|
39
|
+
import { loadSpendBudgets } from "../spend/budgets.js";
|
|
40
|
+
import { loadSpendCheckpoints } from "../spend/checkpoints.js";
|
|
41
|
+
import { loadModelPolicy } from "../model/policy.js";
|
|
42
|
+
import { loadBlobHosting } from "../blob/hosting.js";
|
|
43
|
+
import { loadPrincipals } from "../identity/principals.js";
|
|
44
|
+
import { loadGroups } from "../identity/groups.js";
|
|
45
|
+
// Side effect: register the operator's per-surface trust registry (identity/trust.ts) at
|
|
46
|
+
// boot, before any binding resolves. Self-registering (calls registerSurfaces on
|
|
47
|
+
// import); nothing to thread through the AppConfig object.
|
|
48
|
+
//
|
|
49
|
+
// 🔴 IT ALSO REGISTERS THIS DEPLOYMENT'S OWN IDENTITY, and that half is silent when it lapses.
|
|
50
|
+
// `surfaces.ts` ends with `registerDeploymentIdentity(deploymentIdentity)`, and OPE-1545 deleted the
|
|
51
|
+
// `OE_ORG`/`OE_WORKSPACE`/`OE_SURFACE` reads that used to mask its absence. Drop or reorder this one
|
|
52
|
+
// import and the org, the ambient workspace and the ambient room all fall back to the shipped
|
|
53
|
+
// defaults — every partition key silently becomes `default`, with nothing raised.
|
|
54
|
+
// No check in this app catches that, so keep this import.
|
|
55
|
+
import "../identity/surfaces.js";
|
|
56
|
+
// Side effect: register this deployment's BROWSER HOST DECLARATION (agent/access/browser-hosts.ts) —
|
|
57
|
+
// which hosts the in-sandbox browser may drive, under which verbs, with whose credential, and where
|
|
58
|
+
// page scripting is armed (OPE-1614/OPE-1624). Same self-registering shape as `surfaces.js` above,
|
|
59
|
+
// and it must land HERE rather than in register-adapters.ts: three readers consult it and only two
|
|
60
|
+
// of them run after the adapter wiring — the adapter's own arming check, the firewall compiler
|
|
61
|
+
// (access/egress.ts), and the SANDBOX TEMPLATE BAKE, which reaches `loadAgentBrowserConfig()` from
|
|
62
|
+
// `agent/sandbox/sandbox.ts` through this module's import and nothing else. Drop this import and the
|
|
63
|
+
// declaration is empty at bake time, so the capability silently refuses to arm.
|
|
64
|
+
import "../access/browser-hosts.js";
|
|
65
|
+
// Side effect: register the operator's per-AUTHOR trust register (identity/author-trust.ts) — the
|
|
66
|
+
// AUTHORITY plane, the sibling of the surfaces AUDIENCE plane above. Self-registering, and it must
|
|
67
|
+
// come AFTER the groups module loads (it expands `{ group: … }` members through loadGroups at
|
|
68
|
+
// register time); `../identity/groups.js` is imported above, so by this line the group registry is
|
|
69
|
+
// validated and up.
|
|
70
|
+
import "../identity/authors.js";
|
|
71
|
+
// Side effect: register this deployment's PRE-RUN COLLECTORS and CONDITION GATES (OPE-151,
|
|
72
|
+
// routines/collector.ts + routines/gate.ts). Self-registering, and it must be up before the FIRST
|
|
73
|
+
// dispatcher tick and before `create_routine` validates a proposal — both resolve a name against
|
|
74
|
+
// these menus, and an unregistered menu refuses every name rather than firing ungated, so a dropped
|
|
75
|
+
// import degrades loudly (a routine that names a collector stops being creatable) rather than
|
|
76
|
+
// silently widening anything.
|
|
77
|
+
import "../routines/collectors.js";
|
|
78
|
+
|
|
79
|
+
// The BEHAVIOUR half of config-as-code (OPE-371), registered FIRST — before registerConfig, and
|
|
80
|
+
// before the boot-time cross-validations below — because a settings-sensitive gate consulted during
|
|
81
|
+
// that validation must see this deployment's opinion, not the shipped default. Its own validation
|
|
82
|
+
// already ran at import (defineSettings throws on a bad value), so by this line the object is
|
|
83
|
+
// known-good. Absent registration is a valid state (core falls back to the shipped defaults), which
|
|
84
|
+
// is why this is not fail-loud the way registerConfig is.
|
|
85
|
+
registerSettings(SETTINGS_OVERRIDES);
|
|
86
|
+
|
|
87
|
+
// The PRICING half of config-as-code (OPE-1148), registered beside settings rather than through
|
|
88
|
+
// registerConfig below, for one concrete reason: `getConfig()` THROWS when nothing has registered,
|
|
89
|
+
// and pricing is read on the spend WRITE path (spend/store.ts usdMicrosAtWrite) — a core-only
|
|
90
|
+
// process with no app config must resolve prices from core's BUILT_IN table, not crash. Its own
|
|
91
|
+
// validation already ran at import (assertModelPricingValid throws on a bad rate), so by this line
|
|
92
|
+
// the table is known-good. Registered EARLY, before the cross-validations below, so nothing that
|
|
93
|
+
// prices during boot sees a half-wired deployment.
|
|
94
|
+
registerModelPricing(loadModelPricing());
|
|
95
|
+
|
|
96
|
+
registerConfig({
|
|
97
|
+
loadBundles,
|
|
98
|
+
loadBindings,
|
|
99
|
+
loadNetworkAccess,
|
|
100
|
+
loadConnectorCredentials,
|
|
101
|
+
pluginRegistry: PLUGIN_REGISTRY,
|
|
102
|
+
skillContent: SKILL_CONTENT,
|
|
103
|
+
loadDomains,
|
|
104
|
+
loadSilos,
|
|
105
|
+
loadRoles,
|
|
106
|
+
domainsForSurface,
|
|
107
|
+
loadWebhooks,
|
|
108
|
+
loadSpendBudgets,
|
|
109
|
+
loadSpendCheckpoints,
|
|
110
|
+
loadAccessRequests,
|
|
111
|
+
loadModelPolicy,
|
|
112
|
+
loadBlobHosting,
|
|
113
|
+
loadPrincipals,
|
|
114
|
+
loadGroups,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Also feed the SAME catalogue list into core's lightweight registry (access/connector-credentials.ts).
|
|
118
|
+
// The pure resolver gets the catalogues as an explicit arg, but core-side host paths that run outside
|
|
119
|
+
// a resolveAccess call — the channel/webhook Notion REST reads (channels/notion-api.ts
|
|
120
|
+
// resolveNotionApiKey) — never see it, so they read the notion `channel-key` credential's custody
|
|
121
|
+
// stamp from here instead of a hard-coded env pin. Same array, same wiring point ⇒ one source of
|
|
122
|
+
// truth (register at boot, before any webhook/tool resolves a Notion key).
|
|
123
|
+
setRegisteredConnectorCredentials(loadConnectorCredentials());
|
|
124
|
+
|
|
125
|
+
// Cross-validate at boot, fail LOUD: every domain/role model route (concrete id or tier)
|
|
126
|
+
// must fit the org model-policy envelope — a reviewed git config naming a disallowed model
|
|
127
|
+
// or an unfulfillable tier is a mistake, caught here before the first turn (no-op when no
|
|
128
|
+
// policy is configured). Domains carry role defaults already (applyRoleDefaults inside
|
|
129
|
+
// loadDomains); roles are validated too since a role rung can carry its own route.
|
|
130
|
+
assertRoutesWithinPolicy(loadDomains(), loadRoles(), loadModelPolicy());
|
|
131
|
+
|
|
132
|
+
// Multi-source WINDOW vouch (memory/domain.ts assertWindowSourcesVouched, window-tool-gating-design
|
|
133
|
+
// §Q5): any standing WINDOW holding more than one source requires every register-gated source to be
|
|
134
|
+
// operator-vouched. The unit is the window, not the domain (OPE-1167) — a `mode: "solo"` domain
|
|
135
|
+
// whose named `windows` group merges two channels is checked too.
|
|
136
|
+
// This MUST run HERE, not as a domains.ts module-load side-effect alongside
|
|
137
|
+
// assertUnifiedDomainTrust — the vouch register is populated by the `surfaces.js` import
|
|
138
|
+
// ABOVE, and domains.js is imported BEFORE it (this file's import order), so a check reading the
|
|
139
|
+
// register at domains.js load time would see an EMPTY set and false-fail every source. By this line
|
|
140
|
+
// both are up: domains loaded AND the register registered. The vouch is passed as a pure input
|
|
141
|
+
// (`isVouchedInternal`, evaluated by the assertion at THIS call), never read as a singleton inside
|
|
142
|
+
// the assertion (no desync).
|
|
143
|
+
//
|
|
144
|
+
// 🔴 It passes the RESOLVER, not `vouchedSurfaces()` (OPE-489). A flat key list is matched
|
|
145
|
+
// either-grain, so a surface declared `public` UNDER an internal tenant would still find its tenant
|
|
146
|
+
// key in the list and pass — joining a private cross-surface window the operator had just declared
|
|
147
|
+
// open to the world. The resolver applies most-specific-wins and honours the carve-out.
|
|
148
|
+
assertWindowSourcesVouched(loadDomains(), isVouchedInternal);
|
|
149
|
+
|
|
150
|
+
// Governance venue config lint (OPE-110, memory/domain.ts assertGovernanceDomains / admin-
|
|
151
|
+
// governance-design-2026-07-24.md §4.1): a `governance: true` domain must be an explicit
|
|
152
|
+
// channels domain with a non-empty admins roster, ≤ GOVERNANCE_CHANNEL_MAX channels, all
|
|
153
|
+
// operator-vouched-internal. Runs HERE for the SAME boot-order reason as the window vouch above
|
|
154
|
+
// (the vouch register is populated by the surfaces.js import; by this line both are up), and it
|
|
155
|
+
// takes the same RESOLVER for the same public-carve-out reason.
|
|
156
|
+
assertGovernanceDomains(loadDomains(), isVouchedInternal);
|
|
157
|
+
|
|
158
|
+
// Routine report-channel config lint (OPE-148, memory/domain.ts assertReportChannels): a
|
|
159
|
+
// `reportChannel` must sit on an explicit channels domain and name one of that domain's OWN
|
|
160
|
+
// channels, so a redirected routine can never post to an audience outside the domain it belongs
|
|
161
|
+
// to. Whether the named surface is POSTABLE is a registry question checked at create time instead
|
|
162
|
+
// (boot order makes it unanswerable here) — see schedules/routine-dispatch.ts.
|
|
163
|
+
assertReportChannels(loadDomains());
|
|
164
|
+
|
|
165
|
+
// Compliance-mode config lint (OPE-54 leg 2, memory/domain.ts assertComplianceDomains /
|
|
166
|
+
// admin-governance-design-2026-07-24.md §7): `requestorEntitlementChecks` is an ORG-LEVEL opt-in and
|
|
167
|
+
// may appear ONLY on the `org` pseudo-domain — a per-room placement is refused loudly rather than
|
|
168
|
+
// silently ignored (a silently-ignored security opt-in is worse than none). Pure config check, no
|
|
169
|
+
// vouch register needed, but kept alongside its sibling lints for one obvious boot-validation block.
|
|
170
|
+
assertComplianceDomains(loadDomains());
|
|
171
|
+
|
|
172
|
+
// Per-domain credential override lint (OPE-467, access/domain-credentials.ts): a domain that binds
|
|
173
|
+
// a connector to its OWN credential must name a credential that connector actually declares. Runs
|
|
174
|
+
// HERE because it is the one place both the domain list and the catalogue list are in hand. It must
|
|
175
|
+
// fail LOUD: a bound domain never falls back to the org credential, so an unresolvable binding
|
|
176
|
+
// would otherwise surface as a domain that silently fails every call to that vendor.
|
|
177
|
+
assertDomainCredentialsResolvable(loadDomains(), loadConnectorCredentials());
|
|
178
|
+
|
|
179
|
+
// Silent room-grain reachability signal (OPE-818, memory/domain.ts warnSilentRoomGrainWithoutPublicSignal's
|
|
180
|
+
// own doc comment for why this WARNS rather than fails boot). Runs HERE, after the surfaces.js
|
|
181
|
+
// import above, for the same boot-order reason as the vouch checks: declaredTrustForKeys reads the
|
|
182
|
+
// live trust register that import populates.
|
|
183
|
+
warnSilentRoomGrainWithoutPublicSignal(loadDomains(), resolveExternalPolicy().roomGrain, (workspaceKey, surfaceKey) =>
|
|
184
|
+
declaredTrustForKeys(workspaceKey, surfaceKey) === "public",
|
|
185
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The AGENTS OVERLAY loader — the runtime half of the create_agent flow's config foundation
|
|
2
|
+
// (increment 1). `agents.generated.ts` (next to this file) is a MACHINE-OWNED, DATA-ONLY overlay of
|
|
3
|
+
// extra memory domains + silos + access bundles/bindings (and the frozen AgentSpec side-table that
|
|
4
|
+
// derived them) that merges into the config-as-code baseline — domains.ts (domains + silos) and
|
|
5
|
+
// bundles.ts (bundles + bindings) — at load. Nothing edits it by hand in the normal flow: the
|
|
6
|
+
// deterministic codemod (core memory/agents-codemod.ts) writes it on a create-agent branch, a bot-PR
|
|
7
|
+
// carries the diff, and GitHub's merge button is the approval gate — exactly like the grants overlay
|
|
8
|
+
// (agent/access/grants.ts).
|
|
9
|
+
//
|
|
10
|
+
// WHY a static import (not an fs read): Eve's bundler inlines the module into the deploy artifact, so
|
|
11
|
+
// the overlay can never be silently absent at runtime. Parsing is STRICT + fail-loud (parseAgentsFile):
|
|
12
|
+
// a malformed overlay refuses to boot rather than loading as "roughly right", and Vercel keeps the
|
|
13
|
+
// previous deployment live on a failed build/boot. This loader does not prove the `.ts` module is
|
|
14
|
+
// data. In this app the person who merges the codemod's PR is the only gate on that — see
|
|
15
|
+
// agent/access/grants.ts.
|
|
16
|
+
|
|
17
|
+
import { parseAgentsFile, type AgentsFile } from "@open-everywhere/core/agent/memory/agents-codemod.js";
|
|
18
|
+
import agentsOverlay from "./agents.generated.js";
|
|
19
|
+
|
|
20
|
+
const overlay: AgentsFile = parseAgentsFile(agentsOverlay);
|
|
21
|
+
|
|
22
|
+
/** The parsed agents overlay — merged into loadDomains()/loadSilos() (domains.ts) and
|
|
23
|
+
* loadBundles()/loadBindings() (bundles.ts). */
|
|
24
|
+
export function loadAgentsOverlay(): AgentsFile {
|
|
25
|
+
return overlay;
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// GENERATED + MACHINE-OWNED — DO NOT HAND-EDIT.
|
|
2
|
+
//
|
|
3
|
+
// The agents overlay: DATA ONLY. Written exclusively by the deterministic codemod
|
|
4
|
+
// (packages/core/agent/memory/agents-codemod.ts) on a bot-PR branch; the PR merge is the approval gate.
|
|
5
|
+
//
|
|
6
|
+
// The codemod re-emits this file from its own parsed content and refuses to rewrite it when the
|
|
7
|
+
// bytes differ, so a hand-edit stops the next machine PR. Before a merge, nothing checks that the
|
|
8
|
+
// file is still data only unless the repository runs its own check: the person who merges the PR
|
|
9
|
+
// is the gate. Read a PR to this file as code, because it is code.
|
|
10
|
+
//
|
|
11
|
+
// To change it by hand, edit only the data below and keep the layout exactly as emitted.
|
|
12
|
+
|
|
13
|
+
import type { AgentsFile } from "@open-everywhere/core/agent/memory/agents-codemod.js";
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
"domains": [],
|
|
17
|
+
"silos": [],
|
|
18
|
+
"bundles": [],
|
|
19
|
+
"bindings": [],
|
|
20
|
+
"specs": []
|
|
21
|
+
} satisfies AgentsFile;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// DM — an internal DM partner keeps a personal per-user silo, and reads the org brain on top
|
|
2
|
+
// (read-only), so the agent can recall shared facts in a 1:1. Delete this file's entry in
|
|
3
|
+
// domains/index.ts to keep DMs personal-only.
|
|
4
|
+
//
|
|
5
|
+
// `models.primary` routes every internal DM session to the cheaper utility model.
|
|
6
|
+
|
|
7
|
+
import { defineDomain } from "@open-everywhere/core/agent/memory/domain.js";
|
|
8
|
+
import { UTILITY_MODEL } from "../../../model/models.js";
|
|
9
|
+
|
|
10
|
+
export const domain = defineDomain({ name: "dm", match: "dm", includeOrg: "readonly", models: { primary: { id: UTILITY_MODEL } } });
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// THE DOMAIN LOADER — one file per domain, assembled here (OPE-254).
|
|
2
|
+
//
|
|
3
|
+
// Each `domains/<name>.ts` exports ONE `defineDomain(...)` as `domain`, plus an optional
|
|
4
|
+
// `enabled()` predicate for a fixture that is only registered under a flag. This file lists them,
|
|
5
|
+
// and the list is the only reason to edit it: adding a domain is a new file plus one line here.
|
|
6
|
+
//
|
|
7
|
+
// WHY A LIST AND NOT A DIRECTORY SCAN. The deployment builds to a bundle, where a directory read at
|
|
8
|
+
// runtime finds nothing. An explicit list also makes the domain set a reviewable diff: a new domain
|
|
9
|
+
// shows up as a line in this file, never as a file that appeared.
|
|
10
|
+
//
|
|
11
|
+
// WHAT A DOMAIN FILE SAYS. `reach` is the one membership list — an exact key
|
|
12
|
+
// `platform/<tenant>/<surface>`, a whole tenant `platform/<tenant>`, or a same-level glob
|
|
13
|
+
// `platform/<tenant>/<prefix>*` — and `exclude` carves keys back out. MOST SPECIFIC WINS across
|
|
14
|
+
// domains, so an exact key beats a glob and a longer glob beats a shorter one; handing a slice of
|
|
15
|
+
// one domain's tenant to another is written by writing the narrower entry, nothing more.
|
|
16
|
+
// Anything needing a concrete list (approval-card venue, `reportChannel`, a `governance` venue)
|
|
17
|
+
// reads the EXACT entries only. A container hands its domain's MEMORY and sandbox preset to
|
|
18
|
+
// surfaces nobody typed — and, because a domain-scoped binding matches by domain membership,
|
|
19
|
+
// whatever bundles are bound to that domain travel with it. That is a deliberate capability grant,
|
|
20
|
+
// and it is why this lives in code behind a PR: widen a domain's reach and you have widened an
|
|
21
|
+
// access scope.
|
|
22
|
+
//
|
|
23
|
+
// `role` (optional): adopt a named ROLE preset from agent/roles/roles.ts — a reusable set of
|
|
24
|
+
// instructions/persona + themed statusVerbs (+ optionally models/memory/sandbox). A role presets no
|
|
25
|
+
// access knob at all, so it can never widen what a domain reaches. The domain's own fields win,
|
|
26
|
+
// field by field; verbs merge framework ← org ← role ← domain like any other layer.
|
|
27
|
+
// defineDomain({ name: "qa", reach: ["slack/T…/C…"], role: "qa-engineer" })
|
|
28
|
+
// defineDomain({ name: "org", match: "org", role: "butler" }) // workspace-wide voice
|
|
29
|
+
// // explicit fields still win over the role's preset, knob by knob:
|
|
30
|
+
// defineDomain({ name: "qa", reach: ["slack/T…/C…"], role: "qa-engineer",
|
|
31
|
+
// statusVerbs: ["Kicking the tyres"], // EXTENDS the role's themed verbs
|
|
32
|
+
// models: { primary: { reasoning: "high" } } }) // overlays the role's route, field-by-field
|
|
33
|
+
//
|
|
34
|
+
|
|
35
|
+
// --- EXAMPLE: a team domain spanning a couple of channels, with memory + tools ---
|
|
36
|
+
// defineDomain({
|
|
37
|
+
// name: "transactions",
|
|
38
|
+
// reach: ["slack/T_ENG/C_TXN", "slack/T_ENG/C_TXN_EMEA"], // fully-qualified surface keys
|
|
39
|
+
// memory: "transactions-silo", // → a silo above (omit for a grouping with no memory)
|
|
40
|
+
// }),
|
|
41
|
+
//
|
|
42
|
+
// A domain declares memory, not capability — there is no `bundle:` field to set here. To give
|
|
43
|
+
// this domain's channels a bundle, BIND it in agent/access/bundles.ts:
|
|
44
|
+
// defineBinding({ bundle: "transactions-tools", scope: { level: "domain", domain: "transactions" } })
|
|
45
|
+
// The binding names the domain; `bindings` stays the one list resolveAccess reads and the
|
|
46
|
+
// config-diff gate audits.
|
|
47
|
+
//
|
|
48
|
+
// --- EXAMPLE: multi-source — the team's own writable brain PLUS a shared, read-only
|
|
49
|
+
// baseline. Each silo materialises into its own dir (memory/transactions-silo/,
|
|
50
|
+
// memory/org-baseline/); the agent writes to the writable one, reads both. ---
|
|
51
|
+
// defineDomain({
|
|
52
|
+
// name: "transactions",
|
|
53
|
+
// reach: ["slack/T_ENG/C_TXN"],
|
|
54
|
+
// memory: "transactions-silo", // the domain's OWN brain…
|
|
55
|
+
// includeOrg: "readonly", // …PLUS the `org` pseudo-domain's silos as one unit
|
|
56
|
+
// }), // (auto-tracks org; change org once, every domain follows)
|
|
57
|
+
//
|
|
58
|
+
// --- CRITERIA PSEUDO-DOMAINS (optional) — selected by trust/mode, not a channel list.
|
|
59
|
+
// Each is a singleton (at most one per match) with the full domain toolkit.
|
|
60
|
+
//
|
|
61
|
+
// ORG — internal channels in NO explicit domain fall through here (the old "org default";
|
|
62
|
+
// omit this domain to give unassigned channels no shared memory). Per-silo read/write —
|
|
63
|
+
// keep a curated baseline read-only; nothing is forced.
|
|
64
|
+
// defineDomain({ name: "org", match: "org", memory: "org-baseline" }),
|
|
65
|
+
//
|
|
66
|
+
// PUBLIC — externally-shared / guest channels AND external DM partners route here (the
|
|
67
|
+
// trust gate's destination). Read-only by construction (external sessions never write
|
|
68
|
+
// shared memory). Leave it out (or give it no silos) and external surfaces get no memory.
|
|
69
|
+
// defineDomain({ name: "public", match: "public", memory: "external-faq" }),
|
|
70
|
+
//
|
|
71
|
+
// DM — the shared READ layer an INTERNAL DM partner gets on top of their always-present
|
|
72
|
+
// per-user DB silo. Commonly just the org baseline; omit to keep DMs personal-only.
|
|
73
|
+
// defineDomain({ name: "dm", match: "dm", includeOrg: "readonly" }),
|
|
74
|
+
|
|
75
|
+
import type { Domain } from "@open-everywhere/core/agent/memory/domain.js";
|
|
76
|
+
|
|
77
|
+
import * as org from "./org.js";
|
|
78
|
+
import * as dm from "./dm.js";
|
|
79
|
+
|
|
80
|
+
/** One domain module: its `domain`, and the flag that decides whether it is registered at all. */
|
|
81
|
+
export interface DomainModule {
|
|
82
|
+
readonly domain: Domain;
|
|
83
|
+
readonly enabled?: () => boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every domain module, in registration order. The order is not a tiebreak — most-specific-wins
|
|
88
|
+
* decides ownership and config load refuses any pair it cannot order — but it IS the order a
|
|
89
|
+
* deployer reads, and a few callers take a domain's first exact key as its anchor surface.
|
|
90
|
+
*/
|
|
91
|
+
const MODULES: readonly DomainModule[] = [org, dm];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The declared domain set — every module whose `enabled()` holds (absent = always).
|
|
95
|
+
*
|
|
96
|
+
* 🔴 REFUSES A DUPLICATE NAME, here rather than downstream. A domain name is an IDENTIFIER: it keys
|
|
97
|
+
* the operational store's domain rows, the synthetic session-registry key `oe-domain::<name>` and
|
|
98
|
+
* the supervisor token. Two modules exporting one name would put two configurations on one set of
|
|
99
|
+
* rows, and whichever loaded last would silently win. Every other cross-domain invariant — at most
|
|
100
|
+
* one domain per key, no unorderable overlap, every named room routed — is a single assertion over
|
|
101
|
+
* the assembled set in domains.ts, because none of them can be decided one file at a time.
|
|
102
|
+
*/
|
|
103
|
+
export function loadDomainModules(): Domain[] {
|
|
104
|
+
return assembleDomains(MODULES);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** {@link loadDomainModules} over an INJECTED module list — pure, so the refusal above can be tested
|
|
108
|
+
* without a second copy of the deployment's own config. */
|
|
109
|
+
export function assembleDomains(modules: readonly DomainModule[]): Domain[] {
|
|
110
|
+
const out: Domain[] = [];
|
|
111
|
+
const seen = new Map<string, number>();
|
|
112
|
+
for (const [index, m] of modules.entries()) {
|
|
113
|
+
if (m.enabled !== undefined && !m.enabled()) continue;
|
|
114
|
+
const previous = seen.get(m.domain.name);
|
|
115
|
+
if (previous !== undefined) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`open-everywhere: two domain modules declare the name "${m.domain.name}" (entries ${previous} and ${index} of memory/config/domains/index.ts) — ` +
|
|
118
|
+
"a domain name keys the store's domain rows and the session registry, so one name is one domain. Rename one, or delete the duplicate file.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
seen.set(m.domain.name, index);
|
|
122
|
+
out.push(m.domain);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ORG — every internal room that no explicit domain claims shares this one writable brain. Without
|
|
2
|
+
// it, a room resolves to "no domain" and has nowhere durable to save. Start with one shared brain,
|
|
3
|
+
// and carve rooms into their own domains later if you want separate brains.
|
|
4
|
+
//
|
|
5
|
+
// `nameDropWake` lets the agent give a judged reply to a top-level message that says its name
|
|
6
|
+
// without an @mention. The framework default is off.
|
|
7
|
+
//
|
|
8
|
+
// `admins` grants org-admin standing: the control plane's org-wide tools, approval cards and the
|
|
9
|
+
// spend-cap bypass. Entries are QUALIFIED principal keys. A Slack member id is issued per
|
|
10
|
+
// workspace, so a Slack entry carries the workspace: `slack/<T… team id>/<U… member id>`.
|
|
11
|
+
// admins: ["slack/<team-id>/<member-id>"],
|
|
12
|
+
//
|
|
13
|
+
// `models` routes the model per room group. It inherits org → explicit domain, field by field.
|
|
14
|
+
// `models.supervisor` routes the always-on ambient channel supervisor (opt-in per domain with
|
|
15
|
+
// `ambientSupervisor`) to a cheap tier, without changing the engaged thread sessions.
|
|
16
|
+
|
|
17
|
+
import { defineDomain } from "@open-everywhere/core/agent/memory/domain.js";
|
|
18
|
+
import { SUPERVISOR_MODEL } from "../../../model/models.js";
|
|
19
|
+
|
|
20
|
+
export const domain = defineDomain({
|
|
21
|
+
name: "org",
|
|
22
|
+
match: "org",
|
|
23
|
+
memory: "org-brain",
|
|
24
|
+
nameDropWake: true,
|
|
25
|
+
admins: [],
|
|
26
|
+
models: { supervisor: { id: SUPERVISOR_MODEL, reasoning: "low" } },
|
|
27
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Memory configuration — config-as-code, the admin console for the opt-in memory model
|
|
2
|
+
// (docs/capabilities/memory.md in the Open Everywhere repository). This file ASSEMBLES and VALIDATES; it declares no domain.
|
|
3
|
+
// - DOMAINS — one file each, in memory/config/domains/, listed by that directory's index.ts.
|
|
4
|
+
// A domain is the anchor: which channels share one brain and where it lives (a
|
|
5
|
+
// silo). A channel belongs to AT MOST ONE domain (enforced at load by
|
|
6
|
+
// assertChannelExclusivity). What those channels can DO is NOT set there:
|
|
7
|
+
// capability comes from a domain-scoped BINDING in agent/access/bundles.ts
|
|
8
|
+
// (`{ level: "domain", domain: "<name>" }`), which names the domain rather
|
|
9
|
+
// than being named by it — one grant path, one audited list.
|
|
10
|
+
// - SILOS — reusable backings (where memory physically lives). Referenced by name. Declared
|
|
11
|
+
// here, because a silo is shared BETWEEN domains and belongs to no one of them.
|
|
12
|
+
//
|
|
13
|
+
// ONE LIST SAYS WHAT A DOMAIN REACHES (OPE-254), and domains/index.ts documents it in full:
|
|
14
|
+
// reach: ["github/acme/api", "slack/T_CO/C_ENG"] // exact keys — the enumerable half
|
|
15
|
+
// reach: ["github/acme"] // a whole tenant, incl. repos made tomorrow
|
|
16
|
+
// reach: ["github/acme/payments-*"] // …or a same-level glob, strictly narrower
|
|
17
|
+
// exclude: ["github/acme/payments-secrets"] // …minus these; exclude ALWAYS wins
|
|
18
|
+
// MOST SPECIFIC WINS: an exact key beats a container, a longer container beats a shorter one. So a
|
|
19
|
+
// room named outright can never be annexed by a broader pattern, and two domains splitting one
|
|
20
|
+
// tenant just write the narrower entry. Reach for the glob before the tenant: same ergonomics,
|
|
21
|
+
// strictly less reach.
|
|
22
|
+
//
|
|
23
|
+
// Memory is OPT-IN: with nothing declared, channels have no shared memory (the agent still
|
|
24
|
+
// responds; DMs still get personal memory). Give a channel memory by putting it in a domain.
|
|
25
|
+
// This file is the seam a DB/dashboard would swap behind.
|
|
26
|
+
//
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// SAFETY: an externally-shared / guest-bearing channel never reaches an internal domain,
|
|
29
|
+
// even if listed in one — the trust gate (memory/resolve.ts) routes it to the read-only
|
|
30
|
+
// `public` pseudo-domain (empty unless you curate it); config can't override this. External
|
|
31
|
+
// sessions never WRITE shared memory. DM/personal memory is always DB-pinned, never git/Notion.
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
domainForChannel,
|
|
36
|
+
assertChannelExclusivity,
|
|
37
|
+
assertDomainsValid,
|
|
38
|
+
assertUnifiedDomainTrust,
|
|
39
|
+
assertWindowGroupExclusivity,
|
|
40
|
+
type Domain,
|
|
41
|
+
} from "@open-everywhere/core/agent/memory/domain.js";
|
|
42
|
+
import { assertSingleTenantPerTenantScopedPlatform } from "@open-everywhere/core/agent/access/tenant-admission.js";
|
|
43
|
+
import { defineMemorySilo, assertSilosValid, type MemorySilo } from "@open-everywhere/core/agent/memory/silo.js";
|
|
44
|
+
import { assertExposedAgentsCurated } from "@open-everywhere/core/agent/memory/agents-codemod.js";
|
|
45
|
+
import { applyRoleDefaults } from "@open-everywhere/core/agent/roles/role.js";
|
|
46
|
+
import { loadRoles } from "../../roles/roles.js";
|
|
47
|
+
import { loadAgentsOverlay } from "./agents-overlay.js";
|
|
48
|
+
import { loadDomainModules } from "./domains/index.js";
|
|
49
|
+
|
|
50
|
+
const silos: MemorySilo[] = [
|
|
51
|
+
// The workspace's shared brain. Postgres-backed (matches the deploy:
|
|
52
|
+
// OE_MEMORY_PROVIDER=postgres + Neon DATABASE_URL). Referenced by the `org`
|
|
53
|
+
// domain below, so every internal channel writes to / reads from one brain.
|
|
54
|
+
defineMemorySilo({ name: "org-brain", provider: "postgres" }),
|
|
55
|
+
|
|
56
|
+
// --- EXAMPLE: a DB-backed silo (the default substrate; works out of the box) ---
|
|
57
|
+
// defineMemorySilo({ name: "transactions-silo", provider: "postgres" }),
|
|
58
|
+
//
|
|
59
|
+
// --- EXAMPLE: a git-repo-backed silo (Phase B) ---
|
|
60
|
+
// defineMemorySilo({ name: "transactions-silo", provider: "github", container: "acme/brain", path: "transactions" }),
|
|
61
|
+
//
|
|
62
|
+
// --- EXAMPLE: Notion memory = a read-only MATERIALISED HOT-CACHE on disk (THE Notion form) ---
|
|
63
|
+
// Notion has ONE memory mode: the page subtree is pulled to MD files on disk (the read path —
|
|
64
|
+
// context-light, offline, grep-able), kept READ-ONLY (edits are blocked fail-loud so the agent
|
|
65
|
+
// can't lose updates). To UPDATE, the agent uses the Notion tools (named by `skill`) — never by
|
|
66
|
+
// editing the cache; the cache then refreshes from Notion. `description` + `skill` surface in the
|
|
67
|
+
// top-level memory index. (docs/capabilities/memory.md in the Open Everywhere repository "Notion hot-cache".)
|
|
68
|
+
// defineMemorySilo({ name: "org-baseline", description: "company handbook", provider: "notion",
|
|
69
|
+
// container: "<root-page-id>", readOnly: true, skill: "notion" }),
|
|
70
|
+
//
|
|
71
|
+
// --- EXAMPLE: a CURATED SUBSET of one source in a single silo (memory/selection.ts) ---
|
|
72
|
+
// Take the root page + the "Handbook" subtree, but NOT the noisy "Archive" sibling. `default`
|
|
73
|
+
// is "include" (take only the root + ticked nodes) or "recurse" (take all, prune with excludes);
|
|
74
|
+
// rule paths are relative to the root (or a Notion page id). Also works for github folders.
|
|
75
|
+
// defineMemorySilo({ name: "curated", provider: "notion", container: "<root-page-id>", readOnly: true,
|
|
76
|
+
// select: { default: "include", rules: [{ path: "Handbook", mode: "recurse" }] } }),
|
|
77
|
+
//
|
|
78
|
+
// NB: every silo is materialised (files on disk) — there is no "live, not-on-disk" memory mode.
|
|
79
|
+
// A live external source the agent reads/writes is a connector + a bundle instruction (access
|
|
80
|
+
// plane), not a memory silo. Notion memory = the materialised read-only hot-cache above.
|
|
81
|
+
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// The AGENTS OVERLAY (create_agent flow, increment 1) — a MACHINE-OWNED, DATA-ONLY overlay of extra
|
|
85
|
+
// domains + silos (and the frozen AgentSpec side-table), written by the deterministic create_agent
|
|
86
|
+
// codemod and approved via GitHub's merge button, exactly like the grants overlay in bundles.ts. It
|
|
87
|
+
// merges ADDITIVELY here BEFORE the asserts, so the same load-time validation covers overlay-derived
|
|
88
|
+
// domains/silos, and the empty overlay is a byte-for-byte no-op.
|
|
89
|
+
const agentsOverlay = loadAgentsOverlay();
|
|
90
|
+
const rawDomains: Domain[] = loadDomainModules();
|
|
91
|
+
const mergedRawDomains: Domain[] = [...rawDomains, ...agentsOverlay.domains];
|
|
92
|
+
const mergedSilos: MemorySilo[] = [...silos, ...agentsOverlay.silos];
|
|
93
|
+
|
|
94
|
+
// Resolve role presets at config load: each domain's `role` overlays its single-valued
|
|
95
|
+
// defaults (memory/sandbox; the domain's own field wins) so every downstream consumer sees
|
|
96
|
+
// a plain Domain — a role is sugar, and presets no access knob at all. THROWS on
|
|
97
|
+
// an unknown role name (the typo'd-`role:` bug class fails loud, like the asserts below).
|
|
98
|
+
const domains: Domain[] = applyRoleDefaults(mergedRawDomains, loadRoles());
|
|
99
|
+
|
|
100
|
+
// Validate once, at module load (fail loud on a misconfiguration) — and over the WHOLE assembled
|
|
101
|
+
// set, because none of these is decidable one domain file at a time: a channel is in ≤1 domain and
|
|
102
|
+
// every room a domain names outright still routes to it; domains are reach-XOR-match with singleton
|
|
103
|
+
// pseudo-domains + valid includeOrg; every silo is materialised (has a provider).
|
|
104
|
+
assertChannelExclusivity(domains);
|
|
105
|
+
assertDomainsValid(domains);
|
|
106
|
+
// TENANT FENCE (OPE-309 phase 0c): on a tenant-scoped platform (Slack today) a person id is issued
|
|
107
|
+
// PER TENANT, while a principal key is only `platform/<id>` — so declaring two workspaces of one
|
|
108
|
+
// vendor makes two different humans share one key, and every matcher over it (admin rosters, wake
|
|
109
|
+
// trust, approval clickers, a DM's personal credentials) can hand one person the other's standing.
|
|
110
|
+
// Refused at boot unless OE_ALLOW_MULTI_TENANT_PRINCIPALS=1 says the operator accepts it.
|
|
111
|
+
assertSingleTenantPerTenantScopedPlatform(domains);
|
|
112
|
+
// Unified cross-surface supervisor (research preview) structural safety: a unified domain must be
|
|
113
|
+
// an explicit team-section with ambientSupervisor on, never a pseudo-domain; the org gate lives
|
|
114
|
+
// only on the org domain (memory/domain.ts assertUnifiedDomainTrust). Inert unless a domain opts in.
|
|
115
|
+
assertUnifiedDomainTrust(domains);
|
|
116
|
+
// NAMED WINDOW GROUPS (OPE-1167): a group name IS the window id, and a window id keys the
|
|
117
|
+
// `channel_session` registry row GLOBALLY — so a name two domains share, or a name that is also a
|
|
118
|
+
// domain name, would put two domains' standing conversations on one row. Refused here, with the
|
|
119
|
+
// within-domain overlap that would make "which window owns this channel" depend on declaration
|
|
120
|
+
// order. Inert unless a domain declares `windows`.
|
|
121
|
+
assertWindowGroupExclusivity(domains);
|
|
122
|
+
assertSilosValid(mergedSilos);
|
|
123
|
+
// create_agent EXPOSURE INVARIANT (memory/agents-codemod.ts): every overlay agent whose frozen spec
|
|
124
|
+
// is `exposed` must stay curated read-only — its domain never inherits the org brain or becomes a
|
|
125
|
+
// criteria pseudo-domain, and every silo it reads is readOnly. Keyed off the overlay's own `specs`
|
|
126
|
+
// side-table (a Domain carries no `exposed` field in the MVP). Inert while the overlay is empty.
|
|
127
|
+
assertExposedAgentsCurated(domains, mergedSilos, agentsOverlay.specs);
|
|
128
|
+
|
|
129
|
+
/** The seam a DB/dashboard would swap behind. Static config + agents overlay. */
|
|
130
|
+
export function loadDomains(): readonly Domain[] {
|
|
131
|
+
return domains;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Reusable memory silos (code config + agents overlay). */
|
|
135
|
+
export function loadSilos(): readonly MemorySilo[] {
|
|
136
|
+
return mergedSilos;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- The CODE-ONLY baselines (agents overlay EXCLUDED) — the create_agent PR path
|
|
140
|
+
// (approvals/agent-create.ts) merges a CANDIDATE agents overlay into these for ring-② pre-PR
|
|
141
|
+
// re-validation of the MEMORY half, the domain/silo analogue of loadBaseBundles/loadBaseBindings.
|
|
142
|
+
// Role presets are applied (same shape boot validates) so an overlay agent's channel colliding with a
|
|
143
|
+
// code domain is caught at execute time, not only at boot. NOT for resolver use — runtime memory
|
|
144
|
+
// resolves against the merged loaders above.
|
|
145
|
+
const baseDomains: Domain[] = applyRoleDefaults(rawDomains, loadRoles());
|
|
146
|
+
export function loadBaseDomains(): readonly Domain[] {
|
|
147
|
+
return baseDomains;
|
|
148
|
+
}
|
|
149
|
+
export function loadBaseSilos(): readonly MemorySilo[] {
|
|
150
|
+
return silos;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Domain names a surface belongs to (≤1 under exclusivity) — consumed by the access
|
|
155
|
+
* layer for `domain`-scoped capability bindings. Derived from the domain registry so
|
|
156
|
+
* memory grouping and access grouping never drift.
|
|
157
|
+
*/
|
|
158
|
+
export function domainsForSurface(
|
|
159
|
+
p: { workspaceKey: string; surface: string },
|
|
160
|
+
list: readonly Domain[] = domains,
|
|
161
|
+
): string[] {
|
|
162
|
+
const channelKey = `${p.workspaceKey}/${p.surface}`;
|
|
163
|
+
const d = domainForChannel(channelKey, list);
|
|
164
|
+
return d ? [d.name] : [];
|
|
165
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The model per ROLE, resolved from the active stack (agent/model/stacks.ts). Import a role's id
|
|
2
|
+
// from here. Do not write a model id literal anywhere else.
|
|
3
|
+
|
|
4
|
+
import type { ModelPolicyAllowEntry } from "@open-everywhere/core/agent/model/policy.js";
|
|
5
|
+
import {
|
|
6
|
+
defaultAgentModelId,
|
|
7
|
+
registerDefaultAgentModelId,
|
|
8
|
+
} from "@open-everywhere/core/agent/model/default-model.js";
|
|
9
|
+
import { getSettings } from "@open-everywhere/core/agent/settings/settings.js";
|
|
10
|
+
import { SETTINGS_OVERRIDES } from "../settings/settings.js";
|
|
11
|
+
import { resolveStack, type ModelRole, type ModelRoleSpec, type ModelStack } from "./stacks.js";
|
|
12
|
+
|
|
13
|
+
export type { ModelRoleSpec, ModelStack } from "./stacks.js";
|
|
14
|
+
|
|
15
|
+
const STACK_ALIAS = getSettings(process.env, SETTINGS_OVERRIDES).models.stack;
|
|
16
|
+
const STACK: ModelStack = resolveStack(STACK_ALIAS, {});
|
|
17
|
+
|
|
18
|
+
// A deploy can retarget the primary (OE_FORCE_MODEL / OE_PRIMARY_MODEL). Core owns that
|
|
19
|
+
// precedence, so read the primary back from it.
|
|
20
|
+
registerDefaultAgentModelId(STACK.primary.id);
|
|
21
|
+
|
|
22
|
+
export const MODELS = {
|
|
23
|
+
primary: { ...STACK.primary, id: defaultAgentModelId() },
|
|
24
|
+
utility: STACK.utility,
|
|
25
|
+
vision: STACK.vision,
|
|
26
|
+
supervisor: STACK.supervisor,
|
|
27
|
+
judge: STACK.judge,
|
|
28
|
+
} as const satisfies Record<string, ModelRoleSpec>;
|
|
29
|
+
|
|
30
|
+
/** The alias of the stack this process resolved — for `/oe status`. */
|
|
31
|
+
export const MODEL_STACK = STACK_ALIAS;
|
|
32
|
+
|
|
33
|
+
export const PRIMARY_MODEL = MODELS.primary.id;
|
|
34
|
+
export const UTILITY_MODEL = MODELS.utility.id;
|
|
35
|
+
export const VISION_MODEL = MODELS.vision.id;
|
|
36
|
+
export const SUPERVISOR_MODEL = MODELS.supervisor.id;
|
|
37
|
+
export const JUDGE_MODEL = MODELS.judge.id;
|
|
38
|
+
export const PRIMARY_MODEL_CONTEXT_WINDOW_TOKENS = MODELS.primary.contextWindowTokens;
|
|
39
|
+
|
|
40
|
+
/** One allow-list entry per distinct id in `stack`. The vision model is not selectable as a turn model. */
|
|
41
|
+
export function allowListForStack(stack: ModelStack, extra: readonly ModelPolicyAllowEntry[] = []): ModelPolicyAllowEntry[] {
|
|
42
|
+
const out: ModelPolicyAllowEntry[] = [];
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
for (const [role, spec] of Object.entries(stack) as [ModelRole, ModelRoleSpec][]) {
|
|
45
|
+
if (seen.has(spec.id)) continue;
|
|
46
|
+
seen.add(spec.id);
|
|
47
|
+
out.push(role === "vision" ? { id: spec.id, tier: spec.tier, selectable: false } : { id: spec.id, tier: spec.tier });
|
|
48
|
+
}
|
|
49
|
+
for (const entry of extra) {
|
|
50
|
+
if (seen.has(entry.id)) continue;
|
|
51
|
+
seen.add(entry.id);
|
|
52
|
+
out.push(entry);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The active stack's allow-list, plus `extra`. */
|
|
58
|
+
export function modelAllowList(extra: readonly ModelPolicyAllowEntry[] = []): ModelPolicyAllowEntry[] {
|
|
59
|
+
return allowListForStack(MODELS, extra);
|
|
60
|
+
}
|