@xemahq/opencode-xema-plugin 0.1.5 → 0.1.6
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/package.json +2 -3
- package/plugin.mjs +83 -87
- package/dist/tools/xema-studio.d.ts +0 -28
- package/dist/tools/xema-studio.d.ts.map +0 -1
- package/dist/tools/xema-studio.js +0 -220
- package/dist/tools/xema-studio.js.map +0 -1
- package/src/context.ts +0 -154
- package/src/errors.ts +0 -24
- package/src/hooks/session-idle-autocommit.ts +0 -411
- package/src/hooks/system-prompt-overlay.ts +0 -99
- package/src/hooks/workspace-listing.ts +0 -175
- package/src/index.ts +0 -163
- package/src/logger.ts +0 -40
- package/src/opencode-types.ts +0 -107
- package/src/plugin-entry.ts +0 -22
- package/src/session-state.ts +0 -92
- package/src/tools/runtime.ts +0 -7
- package/src/tools/service-token.ts +0 -68
- package/src/tools/shared.ts +0 -16
- package/src/tools/xema-memory.ts +0 -232
package/src/index.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── @xemahq/opencode-xema-plugin ──
|
|
3
|
-
//
|
|
4
|
-
// Opencode plugin providing Xema-namespaced runtime tools (xema_memory_*)
|
|
5
|
-
// and the dynamic system-prompt overlay hook.
|
|
6
|
-
//
|
|
7
|
-
// Lifecycle:
|
|
8
|
-
// OpenCode evaluates this plugin once at `opencode serve` startup. At that
|
|
9
|
-
// point `/workspace/context.json` does NOT yet exist — it is written per
|
|
10
|
-
// invocation by the orchestrator's XemaRuntimeMounter before the agent
|
|
11
|
-
// starts. The plugin therefore registers all tools unconditionally and
|
|
12
|
-
// defers role validation to tool-execution time, where each tool reads
|
|
13
|
-
// `context.json` from the worker filesystem.
|
|
14
|
-
//
|
|
15
|
-
// Each tool's `execute()` loads session state at call time via
|
|
16
|
-
// `loadSessionStateForTool`, which reads the role's `allowedXemaTools`
|
|
17
|
-
// list straight from `context.json`'s `authority.allowedXemaTools`
|
|
18
|
-
// field (populated by llm-registry-api from the
|
|
19
|
-
// `RoleCapabilityProfile` YAMLs at
|
|
20
|
-
// `biomes/kernel/runtime/role-capabilities/`). Throws a typed error
|
|
21
|
-
// (surfaced to the LLM as a tool error) when the invoking role is not
|
|
22
|
-
// allowed.
|
|
23
|
-
//
|
|
24
|
-
// Path-policy enforcement (where the agent may write, what the agent
|
|
25
|
-
// may read) is intentionally NOT done here. It lives in the rails
|
|
26
|
-
// that genuinely enforce it: workspace-proxy's symlink-aware boundary
|
|
27
|
-
// check, the harvester's manifest-driven read (off-manifest writes
|
|
28
|
-
// are orphan bytes), deliverable-specs content validation, the
|
|
29
|
-
// per-allocation gateway JWT, and pod isolation. A process-local
|
|
30
|
-
// tool.execute.before hook is trivially bypassable via `bash` and
|
|
31
|
-
// would be theatre over those rails.
|
|
32
|
-
//
|
|
33
|
-
// See also:
|
|
34
|
-
// - workspace protocol: runtime AGENTS.md written by the orchestrator
|
|
35
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
-
|
|
37
|
-
import { existsSync } from 'node:fs';
|
|
38
|
-
import { join } from 'node:path';
|
|
39
|
-
|
|
40
|
-
import { buildSessionIdleAutocommit } from './hooks/session-idle-autocommit.js';
|
|
41
|
-
import { buildSystemPromptOverlay } from './hooks/system-prompt-overlay.js';
|
|
42
|
-
import { info, isDebug } from './logger.js';
|
|
43
|
-
import type { Hooks, Plugin, ToolDefinition } from './opencode-types.js';
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
buildXemaMemoryGetTool,
|
|
47
|
-
buildXemaMemorySetTool,
|
|
48
|
-
} from './tools/xema-memory.js';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Defensive idempotency cache — keyed by workspace directory.
|
|
52
|
-
*
|
|
53
|
-
* The plugin's header contract says OpenCode calls this factory once at
|
|
54
|
-
* `opencode serve` startup. Out of an abundance of caution (and because
|
|
55
|
-
* `buildSystemPromptOverlay` builds closures that read disk on each
|
|
56
|
-
* invocation), we cache the resolved `Hooks` per directory. Concurrent
|
|
57
|
-
* factory calls return the SAME in-flight promise instead of racing.
|
|
58
|
-
*
|
|
59
|
-
* This guard is precautionary — there is no confirmed observed
|
|
60
|
-
* double-boot in source today. Cost is two map operations; value is
|
|
61
|
-
* that any future OpenCode change that calls the factory twice won't
|
|
62
|
-
* double-register tools or run boot-time side effects twice.
|
|
63
|
-
*/
|
|
64
|
-
const bootCache = new Map<string, Promise<Hooks>>();
|
|
65
|
-
|
|
66
|
-
export const XemaPlugin: Plugin = async (ctx) => {
|
|
67
|
-
const { directory } = ctx;
|
|
68
|
-
const cached = bootCache.get(directory);
|
|
69
|
-
if (cached !== undefined) {
|
|
70
|
-
return cached;
|
|
71
|
-
}
|
|
72
|
-
const booting = doBoot(directory);
|
|
73
|
-
bootCache.set(directory, booting);
|
|
74
|
-
try {
|
|
75
|
-
return await booting;
|
|
76
|
-
} catch (err) {
|
|
77
|
-
// Clear on failure so the next attempt can retry instead of
|
|
78
|
-
// re-returning the rejected promise forever.
|
|
79
|
-
bootCache.delete(directory);
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
async function doBoot(directory: string): Promise<Hooks> {
|
|
85
|
-
const tools = buildAllTools(directory);
|
|
86
|
-
const toolNames = Object.keys(tools).sort((a, b) => a.localeCompare(b));
|
|
87
|
-
const contextPath = join(directory, 'context.json');
|
|
88
|
-
const contextPresentAtBoot = existsSync(contextPath);
|
|
89
|
-
|
|
90
|
-
info(
|
|
91
|
-
`boot: directory=${directory} tools=${toolNames.length} [${toolNames.join(', ')}] ` +
|
|
92
|
-
`context.json=${contextPresentAtBoot ? 'present' : 'absent'} ` +
|
|
93
|
-
`debug=${isDebug() ? 'on' : 'off'}`,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
const hooks: Hooks = {
|
|
97
|
-
tool: tools,
|
|
98
|
-
'experimental.chat.system.transform': buildSystemPromptOverlay(directory),
|
|
99
|
-
// Per-turn auto-commit. Listens for `session.idle` via the
|
|
100
|
-
// generic `event` channel — opencode 1.15.x does not expose a
|
|
101
|
-
// dedicated `session.idle` hook, only the event union. The handler
|
|
102
|
-
// is a no-op for sessions whose context.json does not opt in
|
|
103
|
-
// (`git.autoCommit !== 'enabled'`).
|
|
104
|
-
event: buildSessionIdleAutocommit(directory),
|
|
105
|
-
};
|
|
106
|
-
return hooks;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Test-only: clear the boot cache so unit tests can observe boot()
|
|
111
|
-
* being called fresh. Not exported from the package barrel.
|
|
112
|
-
*/
|
|
113
|
-
export function __resetBootCacheForTests(): void {
|
|
114
|
-
bootCache.clear();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function buildAllTools(workspaceDir: string): Record<string, ToolDefinition> {
|
|
118
|
-
return {
|
|
119
|
-
xema_memory_set: buildXemaMemorySetTool(workspaceDir),
|
|
120
|
-
xema_memory_get: buildXemaMemoryGetTool(workspaceDir),
|
|
121
|
-
// Document Buddy has no propose tool: both kinds (MARKDOWN_DOCUMENT
|
|
122
|
-
// and RICH_DOCUMENT) edit a real working file at
|
|
123
|
-
// `/workspace/document/<slug>.<ext>` and the platform derives the
|
|
124
|
-
// reviewable diff (see the design notes).
|
|
125
|
-
// emit_review / emit_review_brief: deleted. Reviewers now write
|
|
126
|
-
// `deliverables/review.json` via the standard `write` tool — the
|
|
127
|
-
// deliverable-specs framework Zod-validates at harvest using the
|
|
128
|
-
// `reviewer-output` kernel-tier deliverable spec at
|
|
129
|
-
// `biomes/kernel/runtime/deliverable-specs/specs/schema/reviewer-output/`
|
|
130
|
-
// — the single source of truth for the reviewer-agent output shape
|
|
131
|
-
// consumed by `xema/review@v3`.
|
|
132
|
-
//
|
|
133
|
-
// output_surface_rescan + output_surface_status: owned by the kernel
|
|
134
|
-
// output-surface-control plugin (biomes/kernel/output-surface-control/
|
|
135
|
-
// opencode-tools/output-surface-control.ts) and registered as standalone
|
|
136
|
-
// custom tools auto-discovered by OpenCode from .opencode/tools/. The xema plugin no longer owns them.
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export default XemaPlugin;
|
|
141
|
-
|
|
142
|
-
// Named exports for consumers that want programmatic access (tests, etc.)
|
|
143
|
-
export { readInvocationContext } from './context.js';
|
|
144
|
-
export type { InvocationContext, InvocationRole } from './context.js';
|
|
145
|
-
export * from './errors.js';
|
|
146
|
-
export {
|
|
147
|
-
loadSessionState,
|
|
148
|
-
loadSessionStateForTool,
|
|
149
|
-
ensureRoleAllowed,
|
|
150
|
-
ContextMissingError,
|
|
151
|
-
RoleNotAllowedError,
|
|
152
|
-
type SessionState,
|
|
153
|
-
} from './session-state.js';
|
|
154
|
-
// Role-tool allowlist now lives in the per-invocation context.json
|
|
155
|
-
// (`authority.allowedXemaTools`), populated by llm-registry-api from the
|
|
156
|
-
// `RoleCapabilityProfile` YAMLs at biomes/kernel/runtime/role-capabilities/.
|
|
157
|
-
// No re-export from workflow-contracts: the registry constant is gone.
|
|
158
|
-
|
|
159
|
-
// Gate-review contract lives in `@xemahq/kernel-contracts/workflow`
|
|
160
|
-
// (`ReviewSchema`, `ReviewObjectSchema`, `Review`, enum value types).
|
|
161
|
-
// Consumers should import from there directly — the plugin bundles it
|
|
162
|
-
// into its own `plugin.mjs` at build time but does not re-publish the
|
|
163
|
-
// shape on its own surface.
|
package/src/logger.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── opencode-xema-plugin logger ──
|
|
3
|
-
//
|
|
4
|
-
// Prefixed stderr/stdout logger. `info` / `warn` / `error` are always
|
|
5
|
-
// emitted so operators have baseline visibility of plugin boot and
|
|
6
|
-
// context-read failures without needing a flag.
|
|
7
|
-
// `debug` is env-gated to avoid flooding long sessions; set
|
|
8
|
-
// `XEMA_PLUGIN_DEBUG=1` on the worker (already a first-class env var on
|
|
9
|
-
// the opencode-pool container envelope) to see per-hook / per-tool traces.
|
|
10
|
-
//
|
|
11
|
-
// All messages carry the `[opencode-xema-plugin]` prefix so they survive
|
|
12
|
-
// `docker logs` grep-filtering alongside opencode's own stderr and the
|
|
13
|
-
// workspace-proxy's access log.
|
|
14
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
-
|
|
16
|
-
const PREFIX = '[opencode-xema-plugin]';
|
|
17
|
-
|
|
18
|
-
const DEBUG_ENABLED =
|
|
19
|
-
process.env.XEMA_PLUGIN_DEBUG === '1' ||
|
|
20
|
-
process.env.XEMA_PLUGIN_DEBUG === 'true';
|
|
21
|
-
|
|
22
|
-
export function info(msg: string): void {
|
|
23
|
-
// eslint-disable-next-line no-console
|
|
24
|
-
console.log(`${PREFIX} ${msg}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function warn(msg: string): void {
|
|
28
|
-
// eslint-disable-next-line no-console
|
|
29
|
-
console.warn(`${PREFIX} ${msg}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function debug(msg: string): void {
|
|
33
|
-
if (!DEBUG_ENABLED) return;
|
|
34
|
-
// eslint-disable-next-line no-console
|
|
35
|
-
console.log(`${PREFIX} [debug] ${msg}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function isDebug(): boolean {
|
|
39
|
-
return DEBUG_ENABLED;
|
|
40
|
-
}
|
package/src/opencode-types.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── Minimal opencode plugin type shim ──
|
|
3
|
-
//
|
|
4
|
-
// We re-declare only the types we consume at the plugin boundary so this
|
|
5
|
-
// package does not require the full @opencode-ai/plugin package at install
|
|
6
|
-
// time. The runtime contract (exported default function returning Hooks)
|
|
7
|
-
// matches opencode's loader — see /tmp/opencode/packages/plugin/src/index.ts.
|
|
8
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
-
|
|
10
|
-
import type { z } from 'zod';
|
|
11
|
-
|
|
12
|
-
interface ToolContext {
|
|
13
|
-
sessionID: string;
|
|
14
|
-
messageID: string;
|
|
15
|
-
agent: string;
|
|
16
|
-
directory: string;
|
|
17
|
-
worktree: string;
|
|
18
|
-
abort: AbortSignal;
|
|
19
|
-
metadata(input: { title?: string; metadata?: Record<string, unknown> }): void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ToolDefinition<Args extends z.ZodRawShape = z.ZodRawShape> {
|
|
23
|
-
description: string;
|
|
24
|
-
args: Args;
|
|
25
|
-
execute(
|
|
26
|
-
args: z.infer<z.ZodObject<Args>>,
|
|
27
|
-
context: ToolContext,
|
|
28
|
-
): Promise<string>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Subset of opencode's `Model` shape passed to `experimental.chat.system.transform`.
|
|
33
|
-
* The hook fires before each chat call so we can append a Xema platform
|
|
34
|
-
* overlay to the system array; the model is provided to support per-
|
|
35
|
-
* provider tuning later.
|
|
36
|
-
*/
|
|
37
|
-
export interface ExperimentalChatModel {
|
|
38
|
-
providerID?: string;
|
|
39
|
-
modelID?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ExperimentalChatSystemTransformInput {
|
|
43
|
-
sessionID?: string;
|
|
44
|
-
model: ExperimentalChatModel;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface ExperimentalChatSystemTransformOutput {
|
|
48
|
-
system: string[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Generic OpenCode event delivered to the `event` hook. The upstream
|
|
53
|
-
* `Event` type is a discriminated union; we type only the variants we
|
|
54
|
-
* consume (currently just `session.idle`) and accept the rest as
|
|
55
|
-
* `{ type: string; properties?: Record<string, unknown> }` so the
|
|
56
|
-
* plugin survives new event types added by opencode without a typings
|
|
57
|
-
* bump.
|
|
58
|
-
*/
|
|
59
|
-
export type OpencodeSessionIdleEvent = {
|
|
60
|
-
type: 'session.idle';
|
|
61
|
-
properties: { sessionID: string };
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export type OpencodeEvent =
|
|
65
|
-
| OpencodeSessionIdleEvent
|
|
66
|
-
| { type: string; properties?: Record<string, unknown> };
|
|
67
|
-
|
|
68
|
-
export interface Hooks {
|
|
69
|
-
tool?: Record<string, ToolDefinition>;
|
|
70
|
-
/**
|
|
71
|
-
* Fires once per chat call. We append the rendered Xema system overlay
|
|
72
|
-
* to `output.system` so the LLM sees the AWP base layer + deliverable
|
|
73
|
-
* contract + authority + retry context as authoritative platform
|
|
74
|
-
* context — separate from the project-level `/workspace/AGENTS.md`.
|
|
75
|
-
*/
|
|
76
|
-
'experimental.chat.system.transform'?: (
|
|
77
|
-
input: ExperimentalChatSystemTransformInput,
|
|
78
|
-
output: ExperimentalChatSystemTransformOutput,
|
|
79
|
-
) => Promise<void> | void;
|
|
80
|
-
/**
|
|
81
|
-
* Generic opencode event hook. OpenCode 1.15.x exposes session
|
|
82
|
-
* lifecycle as discriminated events on this channel rather than as
|
|
83
|
-
* dedicated hooks. The xema plugin uses it to react to
|
|
84
|
-
* `session.idle` (per-turn auto-commit). All other event types are
|
|
85
|
-
* ignored.
|
|
86
|
-
*/
|
|
87
|
-
event?: (input: { event: OpencodeEvent }) => Promise<void> | void;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface PluginInput {
|
|
91
|
-
directory: string;
|
|
92
|
-
worktree: string;
|
|
93
|
-
serverUrl: URL;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export type Plugin = (input: PluginInput) => Promise<Hooks>;
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The opencode `tool()` helper is a pass-through identity function. We
|
|
100
|
-
* re-declare it here so plugin authors can use `tool({...})` exactly as
|
|
101
|
-
* they would via @opencode-ai/plugin.
|
|
102
|
-
*/
|
|
103
|
-
export function tool<Args extends z.ZodRawShape>(
|
|
104
|
-
input: ToolDefinition<Args>,
|
|
105
|
-
): ToolDefinition<Args> {
|
|
106
|
-
return input;
|
|
107
|
-
}
|
package/src/plugin-entry.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── OpenCode plugin entry point ──
|
|
3
|
-
//
|
|
4
|
-
// OpenCode v1.4.x's plugin loader inspects the ESM module's exports and
|
|
5
|
-
// expects to find a single plugin factory. When the module re-exports
|
|
6
|
-
// additional identifiers (error classes, constants, helper utilities), the
|
|
7
|
-
// loader's heuristic picks the wrong export and fails with
|
|
8
|
-
// "Plugin export is not a function". The tools then silently fail to
|
|
9
|
-
// register — the agent boots, the model is still resolvable, but every
|
|
10
|
-
// prompt that references a `xema_*` tool dies at tool-dispatch time
|
|
11
|
-
// with "Model tried to call unavailable tool".
|
|
12
|
-
//
|
|
13
|
-
// This file is the bundle entry for the worker-image build. It re-exports
|
|
14
|
-
// ONLY the plugin factory (as the default export AND as a named export for
|
|
15
|
-
// loaders that look for either). Everything else — errors, runtime helpers,
|
|
16
|
-
// the role-capability registry — stays available to TypeScript consumers via
|
|
17
|
-
// the package's regular `index.ts` barrel, which is the consumer-facing
|
|
18
|
-
// entry (not the runtime plugin entry). The two entry points let the
|
|
19
|
-
// programmatic API stay rich without breaking OpenCode's plugin contract.
|
|
20
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
-
|
|
22
|
-
export { XemaPlugin as default, XemaPlugin } from './index.js';
|
package/src/session-state.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── Session state + role allowlist ──
|
|
3
|
-
//
|
|
4
|
-
// The xema plugin is evaluated once at `opencode serve` startup. That is
|
|
5
|
-
// BEFORE the orchestrator writes /workspace/context.json (which is done per
|
|
6
|
-
// invocation by XemaRuntimeMounter). Every tool and hook therefore reads
|
|
7
|
-
// context.json lazily at call time through this module.
|
|
8
|
-
//
|
|
9
|
-
// Role-tool allowlist: source of truth is the `RoleCapabilityProfile`
|
|
10
|
-
// YAMLs at `biomes/kernel/runtime/role-capabilities/`, resolved by
|
|
11
|
-
// llm-registry-api's `agent-run-context.service.ts` and written into the
|
|
12
|
-
// `authority.allowedXemaTools` field of context.json. The plugin reads
|
|
13
|
-
// the list straight off context — adding/changing a role's tool list
|
|
14
|
-
// happens in the YAML, never here.
|
|
15
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
readInvocationContext,
|
|
19
|
-
type InvocationContext,
|
|
20
|
-
type InvocationRole,
|
|
21
|
-
} from './context.js';
|
|
22
|
-
|
|
23
|
-
export interface SessionState {
|
|
24
|
-
ctx: InvocationContext;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class ContextMissingError extends Error {
|
|
28
|
-
readonly code = 'XEMA_CONTEXT_MISSING';
|
|
29
|
-
constructor(public readonly workspaceDir: string) {
|
|
30
|
-
super(
|
|
31
|
-
`/workspace/context.json not found in ${workspaceDir}. The xema plugin ` +
|
|
32
|
-
`cannot operate without invocation context — the orchestrator must ` +
|
|
33
|
-
`write context.json before the agent starts.`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class RoleNotAllowedError extends Error {
|
|
39
|
-
readonly code = 'XEMA_ROLE_NOT_ALLOWED';
|
|
40
|
-
constructor(
|
|
41
|
-
public readonly role: InvocationRole,
|
|
42
|
-
public readonly toolName: string,
|
|
43
|
-
public readonly allowedTools: string[],
|
|
44
|
-
) {
|
|
45
|
-
super(
|
|
46
|
-
`Role "${role}" is not allowed to call "${toolName}". Allowed tools ` +
|
|
47
|
-
`for this role: [${allowedTools.join(', ')}].`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Read /workspace/context.json from the worker filesystem. Throws
|
|
54
|
-
* ContextMissingError if context.json is not present — callers inside a
|
|
55
|
-
* tool.execute should let this propagate (opencode turns it into a tool
|
|
56
|
-
* error). Hook callers should treat a missing context as a non-xema
|
|
57
|
-
* session instead.
|
|
58
|
-
*/
|
|
59
|
-
export function loadSessionState(workspaceDir: string): SessionState {
|
|
60
|
-
const ctx = readInvocationContext(workspaceDir);
|
|
61
|
-
if (!ctx) {
|
|
62
|
-
throw new ContextMissingError(workspaceDir);
|
|
63
|
-
}
|
|
64
|
-
return { ctx };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function ensureRoleAllowed(
|
|
68
|
-
ctx: InvocationContext,
|
|
69
|
-
toolName: string,
|
|
70
|
-
): void {
|
|
71
|
-
const allowed = ctx.authority.allowedXemaTools;
|
|
72
|
-
if (!allowed.includes(toolName)) {
|
|
73
|
-
throw new RoleNotAllowedError(
|
|
74
|
-
ctx.invocation.role,
|
|
75
|
-
toolName,
|
|
76
|
-
[...allowed].sort((a, b) => a.localeCompare(b)),
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Load session state and assert the invoking role may call the given tool.
|
|
83
|
-
* Used by every Xema-namespaced tool at the top of its execute().
|
|
84
|
-
*/
|
|
85
|
-
export function loadSessionStateForTool(
|
|
86
|
-
workspaceDir: string,
|
|
87
|
-
toolName: string,
|
|
88
|
-
): SessionState {
|
|
89
|
-
const state = loadSessionState(workspaceDir);
|
|
90
|
-
ensureRoleAllowed(state.ctx, toolName);
|
|
91
|
-
return state;
|
|
92
|
-
}
|
package/src/tools/runtime.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── Tool runtime helpers ──
|
|
3
|
-
// Re-export from the root session-state module. Kept as a thin barrel so
|
|
4
|
-
// each Xema-namespaced tool has a stable import path.
|
|
5
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
-
|
|
7
|
-
export { loadSessionStateForTool } from '../session-state.js';
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── Worker service-token reader ──
|
|
3
|
-
//
|
|
4
|
-
// The opencode worker authenticates against platform services with a
|
|
5
|
-
// per-allocation Keycloak service JWT. The JWT carries the `xema_org_id`
|
|
6
|
-
// org-scope claim that `ServiceTokenGuard` cross-checks against
|
|
7
|
-
// the tenant header (`X-Xema-Org-Id`, legacy `X-Org-Id`) on every
|
|
8
|
-
// downstream call — replay against a different tenant is rejected
|
|
9
|
-
// fail-fast at the receiving service.
|
|
10
|
-
// workspace-proxy — running in the same pod — writes that JWT to a file
|
|
11
|
-
// and refreshes it mid-session via `PUT /control/service-token`.
|
|
12
|
-
//
|
|
13
|
-
// The `xema_*` tools read this file FRESH on every call (never cache it):
|
|
14
|
-
// a cached value would go stale the moment workspace-proxy swaps in a
|
|
15
|
-
// refreshed token, silently producing 401s for the rest of the session.
|
|
16
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
-
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Path the worker service token is written to. Configurable via
|
|
22
|
-
* `XEMA_SERVICE_TOKEN_PATH`; defaults to `/run/xema/service-token`. Must
|
|
23
|
-
* stay aligned with `serviceTokenPath()` in `biomes/workspace-proxy/api/workspace-proxy`.
|
|
24
|
-
*/
|
|
25
|
-
function serviceTokenPath(): string {
|
|
26
|
-
return process.env['XEMA_SERVICE_TOKEN_PATH']?.trim() || '/run/xema/service-token';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Read the worker service JWT from disk, fresh. Throws a clear fail-fast
|
|
31
|
-
* error when the file is missing or empty — the worker cannot reach
|
|
32
|
-
* platform services without it, and silently sending no `Authorization`
|
|
33
|
-
* header would just surface as opaque 401s deeper in the stack.
|
|
34
|
-
*/
|
|
35
|
-
export function readServiceToken(): string {
|
|
36
|
-
const path = serviceTokenPath();
|
|
37
|
-
let raw: string;
|
|
38
|
-
try {
|
|
39
|
-
raw = readFileSync(path, 'utf8');
|
|
40
|
-
} catch (err) {
|
|
41
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
42
|
-
if (code === 'ENOENT') {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`Worker service token file is missing at ${path}. workspace-proxy ` +
|
|
45
|
-
'must write the per-allocation Keycloak JWT (via the session ' +
|
|
46
|
-
'bundle or PUT /control/service-token) before xema_* tools run.',
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
-
throw new Error(`Failed to read worker service token at ${path}: ${message}`);
|
|
51
|
-
}
|
|
52
|
-
const token = raw.trim();
|
|
53
|
-
if (!token) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`Worker service token file at ${path} is empty. workspace-proxy ` +
|
|
56
|
-
'must write a non-empty per-allocation Keycloak JWT.',
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
return token;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Convenience wrapper returning the `Authorization: Bearer <jwt>` header
|
|
64
|
-
* value for the worker service token.
|
|
65
|
-
*/
|
|
66
|
-
export function serviceTokenAuthorizationHeader(): string {
|
|
67
|
-
return `Bearer ${readServiceToken()}`;
|
|
68
|
-
}
|
package/src/tools/shared.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
export function writeOutputFile(
|
|
5
|
-
workspaceDir: string,
|
|
6
|
-
relativePath: string,
|
|
7
|
-
content: string,
|
|
8
|
-
): void {
|
|
9
|
-
const fullPath = join(workspaceDir, relativePath);
|
|
10
|
-
mkdirSync(dirname(fullPath), { recursive: true });
|
|
11
|
-
writeFileSync(fullPath, content, 'utf-8');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function toJsonString(value: unknown): string {
|
|
15
|
-
return JSON.stringify(value, null, 2);
|
|
16
|
-
}
|