@ikenga/contract 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -11
- package/dist/canvas/Canvas.d.ts +7 -0
- package/dist/canvas/Canvas.d.ts.map +1 -0
- package/dist/canvas/Canvas.js +115 -0
- package/dist/canvas/Canvas.js.map +1 -0
- package/dist/canvas/canvas.css +579 -0
- package/dist/canvas/index.d.ts +7 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/index.js +4 -0
- package/dist/canvas/index.js.map +1 -0
- package/dist/canvas/types.d.ts +45 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/canvas/types.js +2 -0
- package/dist/canvas/types.js.map +1 -0
- package/dist/canvas/use-drag-snap.d.ts +33 -0
- package/dist/canvas/use-drag-snap.d.ts.map +1 -0
- package/dist/canvas/use-drag-snap.js +73 -0
- package/dist/canvas/use-drag-snap.js.map +1 -0
- package/dist/canvas/use-pan-zoom.d.ts +32 -0
- package/dist/canvas/use-pan-zoom.d.ts.map +1 -0
- package/dist/canvas/use-pan-zoom.js +161 -0
- package/dist/canvas/use-pan-zoom.js.map +1 -0
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +2 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/portability.d.ts +113 -0
- package/dist/engine/portability.d.ts.map +1 -0
- package/dist/engine/portability.js +17 -0
- package/dist/engine/portability.js.map +1 -0
- package/dist/engine/subagent-transcoder.d.ts +24 -0
- package/dist/engine/subagent-transcoder.d.ts.map +1 -0
- package/dist/engine/subagent-transcoder.js +341 -0
- package/dist/engine/subagent-transcoder.js.map +1 -0
- package/dist/engine.d.ts +574 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +85 -0
- package/dist/engine.js.map +1 -0
- package/dist/host-verbs.d.ts +194 -0
- package/dist/host-verbs.d.ts.map +1 -0
- package/dist/host-verbs.js +15 -0
- package/dist/host-verbs.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +376 -19
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +95 -4
- package/dist/manifest.js.map +1 -1
- package/dist/registry.d.ts +364 -36
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +9 -0
- package/dist/registry.js.map +1 -1
- package/dist/scopes.js +1 -1
- package/dist/scopes.js.map +1 -1
- package/package.json +36 -10
- package/schemas/registry/index-v1.json +11 -0
- package/src/canvas/Canvas.tsx +161 -0
- package/src/canvas/canvas.css +579 -0
- package/src/canvas/index.ts +14 -0
- package/src/canvas/types.ts +48 -0
- package/src/canvas/use-drag-snap.ts +107 -0
- package/src/canvas/use-pan-zoom.ts +211 -0
- package/src/engine/index.ts +2 -0
- package/src/engine/portability.ts +123 -0
- package/src/engine/subagent-transcoder.test.ts +306 -0
- package/src/engine/subagent-transcoder.ts +333 -0
- package/src/host-verbs.ts +207 -0
- package/src/index.ts +1 -0
- package/src/manifest.test.ts +97 -0
- package/src/manifest.ts +109 -4
- package/src/registry.ts +9 -0
- package/src/scopes.ts +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-bridge verb shapes for the agent-ops pkg — the **G-TRIGGER** freeze gate.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the kernel RPC methods in `rpc.ts`, `host.*` verbs are dispatched
|
|
5
|
+
* FE-side in the iframe host (`shell/src/components/pkg/pkg-iframe-host.tsx`)
|
|
6
|
+
* and invoked from the iframe via `app.callServerTool({ name, arguments })`,
|
|
7
|
+
* returning their payload on `res.structuredContent`. These types are the
|
|
8
|
+
* shared contract the shell (WP-09) produces and the agent-ops pkg
|
|
9
|
+
* (WP-08 reads, WP-12 writes) consumes. Frozen so changing them after both
|
|
10
|
+
* sides exist forces a cross-repo re-sync.
|
|
11
|
+
*
|
|
12
|
+
* Gated by `capabilities.agentOps` (see `AgentOpsCapabilitySchema`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Typed failure codes a host.agentOps verb returns on the `{ ok: false }`
|
|
16
|
+
* branch, so the pkg UI can render a specific state rather than a raw string.
|
|
17
|
+
* - `daemon_down` — `~/.agent-ops/daemon.lock` missing or daemon not alive
|
|
18
|
+
* - `unauthorized` — the daemon rejected the token (401) — should not happen
|
|
19
|
+
* in normal operation (the shell reads the live lock)
|
|
20
|
+
* - `not_found` — no job with that id (daemon 404 / config miss)
|
|
21
|
+
* - `disabled` — job is disabled, cannot run-now (daemon 409)
|
|
22
|
+
* - `forbidden` — daemon rejected host/origin/method (403/405)
|
|
23
|
+
* - `io_error` — lock/config file unreadable or unwritable
|
|
24
|
+
* - `error` — any other failure */
|
|
25
|
+
export type AgentOpsErrorCode =
|
|
26
|
+
| 'daemon_down'
|
|
27
|
+
| 'unauthorized'
|
|
28
|
+
| 'not_found'
|
|
29
|
+
| 'disabled'
|
|
30
|
+
| 'forbidden'
|
|
31
|
+
| 'io_error'
|
|
32
|
+
| 'error';
|
|
33
|
+
|
|
34
|
+
export interface AgentOpsErrorResult {
|
|
35
|
+
ok: false;
|
|
36
|
+
code: AgentOpsErrorCode;
|
|
37
|
+
/** HTTP status from the daemon when applicable, else null. */
|
|
38
|
+
status: number | null;
|
|
39
|
+
error: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `host.agentOps.runNow({ jobId })` — fire an out-of-schedule run via the
|
|
43
|
+
* daemon's localhost trigger endpoint. The shell reads the 0600
|
|
44
|
+
* `~/.agent-ops/daemon.lock` for `{ port, secret }` and POSTs
|
|
45
|
+
* `127.0.0.1:<port>/jobs/<jobId>/trigger` with BOTH required headers
|
|
46
|
+
* (`x-agent-ops-token: <secret>` + `x-agent-ops-trigger: 1`). */
|
|
47
|
+
export interface AgentOpsRunNowArgs {
|
|
48
|
+
jobId: string;
|
|
49
|
+
}
|
|
50
|
+
export type AgentOpsRunNowResult =
|
|
51
|
+
| { ok: true; status: number; message: string }
|
|
52
|
+
| AgentOpsErrorResult;
|
|
53
|
+
|
|
54
|
+
/** `host.agentOps.setEnabled({ jobId, enabled })` — flip a job's `enabled`
|
|
55
|
+
* flag in the project-scoped config (`~/.atelier/skill-agent-ops/jobs.json`).
|
|
56
|
+
* The daemon honors it on next config load. Does NOT touch the executor. */
|
|
57
|
+
export interface AgentOpsSetEnabledArgs {
|
|
58
|
+
jobId: string;
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
}
|
|
61
|
+
export type AgentOpsSetEnabledResult =
|
|
62
|
+
| { ok: true; jobId: string; enabled: boolean }
|
|
63
|
+
| AgentOpsErrorResult;
|
|
64
|
+
|
|
65
|
+
/** The editable job fields the CRUD form sends to `host.agentOps.upsertJob`.
|
|
66
|
+
* The host fills JobDefinition defaults (schedule_dialect, timeout_ms, retries,
|
|
67
|
+
* backoff, concurrency_policy) for any omitted field; the daemon's Zod loader
|
|
68
|
+
* is the final validation backstop on next config load. */
|
|
69
|
+
export interface AgentOpsJobInput {
|
|
70
|
+
id: string;
|
|
71
|
+
label: string;
|
|
72
|
+
schedule: string;
|
|
73
|
+
timezone?: string;
|
|
74
|
+
enabled?: boolean;
|
|
75
|
+
mode?: 'agent' | 'script';
|
|
76
|
+
command: string;
|
|
77
|
+
model?: string | null;
|
|
78
|
+
agent?: string | null;
|
|
79
|
+
schedule_dialect?: '5f' | '6f';
|
|
80
|
+
timeout_ms?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `host.agentOps.upsertJob({ job })` — create-or-update a job in the
|
|
84
|
+
* project-scoped config (atomic rewrite; replace by id if present, else
|
|
85
|
+
* append). The daemon honors it on next config load. Validating-only write —
|
|
86
|
+
* the shell does NOT run the job, never becomes the executor. */
|
|
87
|
+
export interface AgentOpsUpsertJobArgs {
|
|
88
|
+
job: AgentOpsJobInput;
|
|
89
|
+
}
|
|
90
|
+
export type AgentOpsUpsertJobResult =
|
|
91
|
+
| { ok: true; jobId: string; created: boolean }
|
|
92
|
+
| AgentOpsErrorResult;
|
|
93
|
+
|
|
94
|
+
/** `host.agentOps.deleteJob({ jobId })` — remove a job from the project-scoped
|
|
95
|
+
* config (atomic rewrite). The daemon stops scheduling it on next load. */
|
|
96
|
+
export interface AgentOpsDeleteJobArgs {
|
|
97
|
+
jobId: string;
|
|
98
|
+
}
|
|
99
|
+
export type AgentOpsDeleteJobResult =
|
|
100
|
+
| { ok: true; jobId: string }
|
|
101
|
+
| AgentOpsErrorResult;
|
|
102
|
+
|
|
103
|
+
/** `host.agentOps.tailRun({ jobId, offset? })` — read the live (or last-completed)
|
|
104
|
+
* run output for a job by byte-range, for the pkg's Live-output view.
|
|
105
|
+
*
|
|
106
|
+
* Unlike the other agent-ops verbs (which reach the daemon's localhost endpoint
|
|
107
|
+
* or rewrite its config), tailRun is a pure **filesystem read on the shell's own
|
|
108
|
+
* event loop**: it opens the per-job marker (`~/.agent-ops/runs/<slug>.marker.json`)
|
|
109
|
+
* to learn the current run's `tailPath` + `status` + `pid`, then reads that tail
|
|
110
|
+
* file from `offset` forward (capped per call). Because it never touches the
|
|
111
|
+
* daemon, the daemon being blocked mid-run (synchronously executing a job) is
|
|
112
|
+
* irrelevant — the shell still streams whatever the child has teed to disk so far.
|
|
113
|
+
*
|
|
114
|
+
* Mechanism is **script-mode only**: the daemon tees a script job's combined
|
|
115
|
+
* stdout/stderr to the tail file as it runs. Agent jobs (`claude -p`) stay
|
|
116
|
+
* byte-for-byte unchanged and have no tail file, so tailRun returns an empty
|
|
117
|
+
* chunk with `mode:'agent'` (the pkg renders 'live output not available for
|
|
118
|
+
* agent jobs' + a spinner driven off the marker's `status`).
|
|
119
|
+
*
|
|
120
|
+
* Polling loop: call with `offset:0` first, then feed the returned `nextOffset`
|
|
121
|
+
* back on each poll. `eof` true means the reader caught up to the current end of
|
|
122
|
+
* file; keep polling while `running` is true. After a run completes the marker
|
|
123
|
+
* still points at the final tail, so `running:false` STILL returns the final
|
|
124
|
+
* chunk for scrollback — the view shows the completed output, not an empty pane.
|
|
125
|
+
*
|
|
126
|
+
* - `running` — `status === 'running'` AND the marker's `pid` is alive.
|
|
127
|
+
* - `status` — the marker's `status` (`'running' | 'done'`), or `null` when
|
|
128
|
+
* no marker exists yet (job has never produced a run).
|
|
129
|
+
* - `startedAtMs` — the run's start epoch-ms from the marker, else `null`.
|
|
130
|
+
* - `mode` — `'script'` (has a tail) / `'agent'` (no tail) / `null` (no marker).
|
|
131
|
+
* - `chunk` — lossy-utf8 decode of the bytes read this call (may be empty).
|
|
132
|
+
* - `nextOffset` — `offset + bytesRead`; pass back on the next poll.
|
|
133
|
+
* - `eof` — reached the current end of the tail file.
|
|
134
|
+
*
|
|
135
|
+
* Best-effort + non-throwing on the shell side: a missing marker / missing tail /
|
|
136
|
+
* unreadable file resolves to `{ ok:true, chunk:'', eof:true, ... }` rather than
|
|
137
|
+
* an error, so the view degrades to 'no output yet'. Only a path-escape attempt
|
|
138
|
+
* (marker.tailPath pointing outside `~/.agent-ops/runs/`) maps to an
|
|
139
|
+
* `{ ok:false, code:'io_error' }`. */
|
|
140
|
+
export interface AgentOpsTailRunArgs {
|
|
141
|
+
jobId: string;
|
|
142
|
+
/** Byte offset into the tail file to read from. Defaults to 0. */
|
|
143
|
+
offset?: number;
|
|
144
|
+
}
|
|
145
|
+
export type AgentOpsTailRunResult =
|
|
146
|
+
| {
|
|
147
|
+
ok: true;
|
|
148
|
+
running: boolean;
|
|
149
|
+
status: 'running' | 'done' | null;
|
|
150
|
+
startedAtMs: number | null;
|
|
151
|
+
mode: 'agent' | 'script' | null;
|
|
152
|
+
chunk: string;
|
|
153
|
+
nextOffset: number;
|
|
154
|
+
eof: boolean;
|
|
155
|
+
}
|
|
156
|
+
| AgentOpsErrorResult;
|
|
157
|
+
|
|
158
|
+
/** A merged config+state row as returned by `host.agentOps.listJobs`. Config
|
|
159
|
+
* fields come from `~/.atelier/skill-agent-ops/jobs.json` (a JobDefinition);
|
|
160
|
+
* `state` is that job's entry in `.company/cron/jobs-state.json` (or null if
|
|
161
|
+
* it has never run). Field casing matches the on-disk files (camelCase state);
|
|
162
|
+
* the pkg's data layer (WP-08) maps this into the snake_case G-VIEW. */
|
|
163
|
+
export interface AgentOpsRawJobState {
|
|
164
|
+
nextRunAtMs: number | null;
|
|
165
|
+
lastRunAtMs: number | null;
|
|
166
|
+
lastStatus: string | null;
|
|
167
|
+
consecutiveErrors: number;
|
|
168
|
+
lastDurationMs: number | null;
|
|
169
|
+
totalCostUsd: number | null;
|
|
170
|
+
totalRuns: number | null;
|
|
171
|
+
lastUsage: {
|
|
172
|
+
costUsd: number | null;
|
|
173
|
+
numTurns: number | null;
|
|
174
|
+
inputTokens: number | null;
|
|
175
|
+
outputTokens: number | null;
|
|
176
|
+
cacheReadTokens: number | null;
|
|
177
|
+
sessionId: string | null;
|
|
178
|
+
} | null;
|
|
179
|
+
}
|
|
180
|
+
export interface AgentOpsRawJob {
|
|
181
|
+
id: string;
|
|
182
|
+
label: string;
|
|
183
|
+
schedule: string;
|
|
184
|
+
schedule_dialect: '5f' | '6f';
|
|
185
|
+
timezone: string;
|
|
186
|
+
enabled: boolean;
|
|
187
|
+
command: string;
|
|
188
|
+
mode: 'agent' | 'script';
|
|
189
|
+
model: string | null;
|
|
190
|
+
agent: string | null;
|
|
191
|
+
_disabledReason: string | null;
|
|
192
|
+
state: AgentOpsRawJobState | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `host.agentOps.listJobs({})` — read the project-scoped job config + the
|
|
196
|
+
* daemon's runtime state file and return both, merged per job, plus daemon
|
|
197
|
+
* liveness. Run history (cron_job_runs / agent_runs) is NOT included here —
|
|
198
|
+
* the pkg reads that directly via `host.dbQuery`. */
|
|
199
|
+
export type AgentOpsListJobsArgs = Record<string, never>;
|
|
200
|
+
export type AgentOpsListJobsResult =
|
|
201
|
+
| {
|
|
202
|
+
ok: true;
|
|
203
|
+
daemon_up: boolean;
|
|
204
|
+
daemon_pid: number | null;
|
|
205
|
+
jobs: AgentOpsRawJob[];
|
|
206
|
+
}
|
|
207
|
+
| AgentOpsErrorResult;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ManifestSchema,
|
|
6
|
+
RequiresEntrySchema,
|
|
7
|
+
RequireSourceSchema,
|
|
8
|
+
} from './manifest.js';
|
|
9
|
+
|
|
10
|
+
// ─── WP-11 — `requires` field (ADR-015 §3) ──────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const BASE = {
|
|
13
|
+
id: 'com.ikenga.studio',
|
|
14
|
+
name: 'Studio',
|
|
15
|
+
version: '0.1.0',
|
|
16
|
+
ikenga_api: '1',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
test('RequiresEntry: full shape parses', () => {
|
|
20
|
+
const e = RequiresEntrySchema.parse({
|
|
21
|
+
kind: 'skill',
|
|
22
|
+
name: '@ikenga/studio-beat-detect',
|
|
23
|
+
source: 'npx',
|
|
24
|
+
ref: 'v1.2.0',
|
|
25
|
+
});
|
|
26
|
+
assert.equal(e.kind, 'skill');
|
|
27
|
+
assert.equal(e.source, 'npx');
|
|
28
|
+
assert.equal(e.ref, 'v1.2.0');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('RequiresEntry: source + ref optional', () => {
|
|
32
|
+
const e = RequiresEntrySchema.parse({ kind: 'skill', name: 'skill-core' });
|
|
33
|
+
assert.equal(e.source, undefined);
|
|
34
|
+
assert.equal(e.ref, undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('RequiresEntry: rejects unknown field (.strict mirrors Rust deny_unknown_fields)', () => {
|
|
38
|
+
assert.throws(() =>
|
|
39
|
+
RequiresEntrySchema.parse({ kind: 'skill', name: 'skill-core', bogus: true }),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('RequiresEntry: kind bundle is accepted (WP-18 G-BUNDLE)', () => {
|
|
44
|
+
// WP-18 locked design decision 4: `RequiresEntrySchema.kind` is `z.string()`
|
|
45
|
+
// (a free string, not a closed enum), so a `requires` entry may reference a
|
|
46
|
+
// bundle — `{kind:"bundle", name}` parses and carries the kind through. This
|
|
47
|
+
// is the contract-side half of the G-BUNDLE "requires kind:bundle parses" DoD.
|
|
48
|
+
const e = RequiresEntrySchema.parse({ kind: 'bundle', name: 'studio-archetypes' });
|
|
49
|
+
assert.equal(e.kind, 'bundle');
|
|
50
|
+
assert.equal(e.name, 'studio-archetypes');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('RequiresEntry: rejects an out-of-set source', () => {
|
|
54
|
+
assert.throws(() =>
|
|
55
|
+
RequiresEntrySchema.parse({ kind: 'skill', name: 'x', source: 'ftp' }),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('RequireSource: only git|npx|catalog|local', () => {
|
|
60
|
+
for (const s of ['git', 'npx', 'catalog', 'local']) {
|
|
61
|
+
assert.equal(RequireSourceSchema.parse(s), s);
|
|
62
|
+
}
|
|
63
|
+
assert.throws(() => RequireSourceSchema.parse('http'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('Manifest: requires parses and round-trips', () => {
|
|
67
|
+
const m = ManifestSchema.parse({
|
|
68
|
+
...BASE,
|
|
69
|
+
requires: [
|
|
70
|
+
{ kind: 'skill', name: '@ikenga/studio-archetypes', source: 'npx' },
|
|
71
|
+
{ kind: 'skill', name: 'skill-core', source: 'git', ref: 'v1.0.0' },
|
|
72
|
+
{ kind: 'skill', name: '@ikenga/studio-doctor' },
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
assert.equal(m.requires.length, 3);
|
|
76
|
+
assert.equal(m.requires[0].name, '@ikenga/studio-archetypes');
|
|
77
|
+
assert.equal(m.requires[2].source, undefined);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('Manifest: requires defaults to [] when absent (pre-Phase-4 manifest)', () => {
|
|
81
|
+
const m = ManifestSchema.parse({ ...BASE });
|
|
82
|
+
assert.deepEqual(m.requires, []);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('Manifest: retired bundling fields are no longer part of the type (WP-17)', () => {
|
|
86
|
+
// ADR-015 decision 4: `skills`/`commands`/`agents` were hard-retired from the
|
|
87
|
+
// schema (lockstep with the Rust `deny_unknown_fields` Manifest, which REJECTS
|
|
88
|
+
// them — the authoritative loader). ManifestSchema is non-strict, so a stray
|
|
89
|
+
// legacy key is stripped rather than rejected here; assert it does not survive
|
|
90
|
+
// onto the parsed object.
|
|
91
|
+
const m = ManifestSchema.parse({ ...BASE, skills: 'skills', commands: 'commands' }) as Record<
|
|
92
|
+
string,
|
|
93
|
+
unknown
|
|
94
|
+
>;
|
|
95
|
+
assert.equal(m.skills, undefined);
|
|
96
|
+
assert.equal(m.commands, undefined);
|
|
97
|
+
});
|
package/src/manifest.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { EngineProvidesSchema } from './engine/index.js';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// v2 (WP-05): added capabilities.sqlite + permissions["sqlite.tables"];
|
|
15
|
+
// permissions["supabase.tables"] kept as a compat alias for api=1 manifests.
|
|
16
|
+
export const IKENGA_API_VERSION = 2 as const;
|
|
15
17
|
export const IKENGA_API_MIN_SUPPORTED = 1 as const;
|
|
16
18
|
|
|
17
19
|
// ---------- Sub-schemas ----------
|
|
@@ -60,6 +62,12 @@ export const PermissionsSchema = z.object({
|
|
|
60
62
|
'fs.read': z.array(z.string()).default([]),
|
|
61
63
|
'fs.write': z.array(z.string()).default([]),
|
|
62
64
|
net: z.array(z.string()).default([]),
|
|
65
|
+
/** Local SQLite table patterns; validated against tables.json at install time.
|
|
66
|
+
* For api ≥ 2 manifests. Replaces `supabase.tables`. */
|
|
67
|
+
'sqlite.tables': z.array(z.string()).default([]),
|
|
68
|
+
/** @deprecated api=1 compat alias for `sqlite.tables`. New manifests should
|
|
69
|
+
* use `sqlite.tables` instead. Kept so ikenga_api="1" manifests parse
|
|
70
|
+
* without errors during the transition window. */
|
|
63
71
|
'supabase.tables': z.array(z.string()).default([]),
|
|
64
72
|
'vault.keys': z.array(z.string()).default([]),
|
|
65
73
|
}).default({});
|
|
@@ -135,6 +143,46 @@ export const CronEntrySchema = z.object({
|
|
|
135
143
|
env_from_settings: z.array(z.string()).default([]),
|
|
136
144
|
});
|
|
137
145
|
|
|
146
|
+
/** Local SQLite capability (api ≥ 2). Threads the logical db name into the
|
|
147
|
+
* iframe host context so the pkg can call `db_query` without hard-coding it.
|
|
148
|
+
* Mirrors `SqliteCapability` in `shell/src-tauri/src/pkg/manifest.rs`. */
|
|
149
|
+
export const SqliteCapabilitySchema = z.object({
|
|
150
|
+
/** Logical DB name. Currently only `"ikenga.local"` is supported.
|
|
151
|
+
* Defaults to `"ikenga.local"` when omitted. */
|
|
152
|
+
db: z.string().default('ikenga.local'),
|
|
153
|
+
});
|
|
154
|
+
export type SqliteCapability = z.infer<typeof SqliteCapabilitySchema>;
|
|
155
|
+
|
|
156
|
+
/** Supabase capability. Mirrors `SupabaseCapability` in
|
|
157
|
+
* `shell/src-tauri/src/pkg/manifest.rs`. */
|
|
158
|
+
export const SupabaseCapabilitySchema = z.object({
|
|
159
|
+
/** When true, mint fails if the Supabase vault keys are missing; when
|
|
160
|
+
* false/omitted, missing keys surface as `supabase: null` in host context. */
|
|
161
|
+
required: z.boolean().default(false),
|
|
162
|
+
});
|
|
163
|
+
export type SupabaseCapability = z.infer<typeof SupabaseCapabilitySchema>;
|
|
164
|
+
|
|
165
|
+
/** Native child-webview capability. Mirrors `WebviewCapability` in
|
|
166
|
+
* `shell/src-tauri/src/pkg/manifest.rs`. */
|
|
167
|
+
export const WebviewCapabilitySchema = z.object({
|
|
168
|
+
/** Whether this pkg may create child webviews via the kernel. Required for
|
|
169
|
+
* any `ui.routes[]` entry with `kind = "webview"` to mount. */
|
|
170
|
+
child_webviews: z.boolean().default(false),
|
|
171
|
+
/** Named cookie/data partitions; empty = the implicit "default" partition. */
|
|
172
|
+
partitions: z.array(z.string()).default([]),
|
|
173
|
+
});
|
|
174
|
+
export type WebviewCapability = z.infer<typeof WebviewCapabilitySchema>;
|
|
175
|
+
|
|
176
|
+
/** Agent-ops host-bridge capability (api ≥ 2). Opt-in to the privileged
|
|
177
|
+
* `host.agentOps.*` verbs (run-now / enable-disable / list-jobs) the shell
|
|
178
|
+
* exposes for the agent-ops observability pkg — these reach the always-on
|
|
179
|
+
* cron daemon's localhost trigger endpoint and read the daemon's config +
|
|
180
|
+
* state files, hops an iframe cannot make itself. Presence of the block is
|
|
181
|
+
* the gate (mirrors the `capabilities.sqlite` opt-in). Mirrors
|
|
182
|
+
* `AgentOpsCapability` in `shell/src-tauri/src/pkg/manifest.rs`. */
|
|
183
|
+
export const AgentOpsCapabilitySchema = z.object({});
|
|
184
|
+
export type AgentOpsCapability = z.infer<typeof AgentOpsCapabilitySchema>;
|
|
185
|
+
|
|
138
186
|
export const WindowBlockSchema = z.object({
|
|
139
187
|
label: z.string(),
|
|
140
188
|
url: z.string(),
|
|
@@ -159,6 +207,39 @@ export const ScreenshotSchema = z.object({
|
|
|
159
207
|
});
|
|
160
208
|
export type Screenshot = z.infer<typeof ScreenshotSchema>;
|
|
161
209
|
|
|
210
|
+
/** Forward-dependency source for a `requires[]` entry. Mirrors the Rust
|
|
211
|
+
* `RequireSource` enum (`pkg/manifest.rs`) and the registry `ProvenanceSource`
|
|
212
|
+
* set. Optional on an entry — absent means the resolver looks the dep up in the
|
|
213
|
+
* store registry / catalog. */
|
|
214
|
+
export const RequireSourceSchema = z.enum(['git', 'npx', 'catalog', 'local']);
|
|
215
|
+
export type RequireSource = z.infer<typeof RequireSourceSchema>;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* One forward-dependency edge (`requires[]` element, ADR-015 §3 / Ọba WP-11).
|
|
219
|
+
* Names a standalone Ọba primitive a pkg `requires`; the resolver (WP-13/14)
|
|
220
|
+
* installs the closure at install/enable. This is a SEPARATE graph from a
|
|
221
|
+
* skill's `SKILL.md` `depends_on` (the G-04 authoring star, `skill-core`-only):
|
|
222
|
+
* a pkg `requires` MAY reference any primitive, and the publish-time lift
|
|
223
|
+
* (WP-12) compiles `depends_on` into this field. `.strict()` mirrors the Rust
|
|
224
|
+
* `deny_unknown_fields` on `RequiresEntry`. Source of truth: the Rust struct in
|
|
225
|
+
* `shell/src-tauri/src/pkg/manifest.rs` — keep in lockstep.
|
|
226
|
+
*/
|
|
227
|
+
export const RequiresEntrySchema = z
|
|
228
|
+
.object({
|
|
229
|
+
/** Primitive kind: skill | agent | command | hook | mcp. Kept a string so a
|
|
230
|
+
* future kind doesn't break old manifests. */
|
|
231
|
+
kind: z.string(),
|
|
232
|
+
/** Primitive name (e.g. `skill-core`, `@ikenga/studio-beat-detect`). */
|
|
233
|
+
name: z.string(),
|
|
234
|
+
/** Optional fetch source. */
|
|
235
|
+
source: RequireSourceSchema.optional(),
|
|
236
|
+
/** Optional git tag/branch or version pin. The shape leaves room for an
|
|
237
|
+
* explicit semver range later without another schema break. */
|
|
238
|
+
ref: z.string().optional(),
|
|
239
|
+
})
|
|
240
|
+
.strict();
|
|
241
|
+
export type RequiresEntry = z.infer<typeof RequiresEntrySchema>;
|
|
242
|
+
|
|
162
243
|
// ---------- Manifest ----------
|
|
163
244
|
|
|
164
245
|
export const ManifestSchema = z.object({
|
|
@@ -176,9 +257,13 @@ export const ManifestSchema = z.object({
|
|
|
176
257
|
targets: z.array(z.string()).default([]),
|
|
177
258
|
|
|
178
259
|
// Capability blocks (all optional — kernel walks present blocks)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
260
|
+
// NOTE (WP-17, ADR-015 decision 4): the `skills`/`commands`/`agents`
|
|
261
|
+
// asset-bundling fields were HARD-RETIRED (lockstep with the Rust
|
|
262
|
+
// `Manifest`). A pkg no longer embeds Claude-config assets; it only
|
|
263
|
+
// `requires` standalone Ọba primitives. The schema is `.strict()`, so a
|
|
264
|
+
// manifest still declaring any of them now FAILS validation (no deprecation
|
|
265
|
+
// window). The shell builtin `com.ikenga.iyke` places its skill/commands by
|
|
266
|
+
// convention from on-disk folders, not via a manifest field.
|
|
182
267
|
mcp: z.array(McpServerSchema).default([]),
|
|
183
268
|
sidecars: z.array(SidecarSpecSchema).default([]),
|
|
184
269
|
permissions: PermissionsSchema,
|
|
@@ -190,6 +275,16 @@ export const ManifestSchema = z.object({
|
|
|
190
275
|
window: WindowBlockSchema.optional(),
|
|
191
276
|
queries: QueriesBlockSchema.optional(),
|
|
192
277
|
|
|
278
|
+
/** Optional capabilities the host resolves and injects at iframe-mount
|
|
279
|
+
* time via the AppBridge `hostContext` handshake. Mirrors the Rust
|
|
280
|
+
* `CapabilitiesBlock` in `shell/src-tauri/src/pkg/manifest.rs`. */
|
|
281
|
+
capabilities: z.object({
|
|
282
|
+
supabase: SupabaseCapabilitySchema.optional(),
|
|
283
|
+
sqlite: SqliteCapabilitySchema.optional(),
|
|
284
|
+
webview: WebviewCapabilitySchema.optional(),
|
|
285
|
+
agentOps: AgentOpsCapabilitySchema.optional(),
|
|
286
|
+
}).optional(),
|
|
287
|
+
|
|
193
288
|
/**
|
|
194
289
|
* Engine-adapter manifest block. Present iff this pkg is an engine-*
|
|
195
290
|
* adapter. Declares the agent id, display name, capability snapshot,
|
|
@@ -205,6 +300,16 @@ export const ManifestSchema = z.object({
|
|
|
205
300
|
* UI (engines, MCP-only servers) typically leave this empty.
|
|
206
301
|
*/
|
|
207
302
|
screenshots: z.array(ScreenshotSchema).default([]),
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Forward dependency declarations (ADR-015 §3 / Ọba WP-11). Each entry names a
|
|
306
|
+
* standalone primitive this pkg `requires`; the Ọba resolver installs the
|
|
307
|
+
* closure at install/enable. A SEPARATE graph from a skill's `depends_on` (the
|
|
308
|
+
* G-04 authoring star). Empty by default so pre-Phase-4 manifests are
|
|
309
|
+
* unaffected. Mirrors `requires: Vec<RequiresEntry>` on the Rust `Manifest`
|
|
310
|
+
* (`pkg/manifest.rs`) — keep in lockstep (`deny_unknown_fields`).
|
|
311
|
+
*/
|
|
312
|
+
requires: z.array(RequiresEntrySchema).default([]),
|
|
208
313
|
});
|
|
209
314
|
|
|
210
315
|
export type Manifest = z.infer<typeof ManifestSchema>;
|
package/src/registry.ts
CHANGED
|
@@ -105,6 +105,15 @@ export const RegistryEntrySchema = z.object({
|
|
|
105
105
|
* Lets the shell render a thumb without fetching the per-pkg detail file.
|
|
106
106
|
*/
|
|
107
107
|
screenshot: z.string().url().optional(),
|
|
108
|
+
/**
|
|
109
|
+
* Catalog visibility. `"hidden"` keeps the pkg installable by exact name
|
|
110
|
+
* (CLI `add`/`update`, registry install, update detection) but omits it
|
|
111
|
+
* from the default browse/catalog surfaces — used for dev/test fixtures
|
|
112
|
+
* and scaffolds (e.g. `pkg-hello`, `pkg-engine-noop`, the cursor-agent
|
|
113
|
+
* scaffold). Absent ⇒ treated as `"public"`. Set by the publish pipeline's
|
|
114
|
+
* curation step (`ikenga-pkgs/scripts/update-registry-index.mjs`).
|
|
115
|
+
*/
|
|
116
|
+
visibility: z.enum(['public', 'hidden']).optional(),
|
|
108
117
|
});
|
|
109
118
|
export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
|
|
110
119
|
|
package/src/scopes.ts
CHANGED
|
@@ -41,7 +41,7 @@ export const SCOPE_CATALOGUE: ScopeDef[] = [
|
|
|
41
41
|
{ scope: 'fs:write', description: 'Write files inside the shell-managed sandbox.', sensitive: true },
|
|
42
42
|
|
|
43
43
|
// engine
|
|
44
|
-
{ scope: 'engine:invoke', description: 'Start sessions and stream from the active AI engine.' },
|
|
44
|
+
{ scope: 'engine:invoke', description: 'Start sessions and stream from the active AI engine.', sensitive: true },
|
|
45
45
|
{ scope: 'engine:register_mcp', description: 'Register additional MCP servers with the engine.', sensitive: true },
|
|
46
46
|
|
|
47
47
|
// shell chrome
|