@lumenflow/cli 3.19.0 → 3.20.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/dist/gates-runners.js +5 -4
- package/dist/gates-runners.js.map +1 -1
- package/dist/gates-utils.js +71 -0
- package/dist/gates-utils.js.map +1 -1
- package/dist/wu-prune.js +2 -2
- package/dist/wu-prune.js.map +1 -1
- package/dist/wu-verify.js +22 -17
- package/dist/wu-verify.js.map +1 -1
- package/package.json +8 -8
- package/packs/agent-runtime/.turbo/turbo-build.log +1 -1
- package/packs/agent-runtime/package.json +1 -1
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/README.md +118 -113
- package/packs/sidekick/manifest-schema.ts +15 -228
- package/packs/sidekick/manifest.ts +107 -7
- package/packs/sidekick/manifest.yaml +199 -1
- package/packs/sidekick/package.json +4 -1
- package/packs/sidekick/policy-factory.ts +38 -0
- package/packs/sidekick/tool-impl/channel-tools.ts +99 -0
- package/packs/sidekick/tool-impl/memory-tools.ts +86 -1
- package/packs/sidekick/tool-impl/routine-tools.ts +156 -2
- package/packs/sidekick/tool-impl/storage.ts +6 -5
- package/packs/sidekick/tool-impl/task-tools.ts +186 -4
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/package.json +1 -1
- package/templates/core/_frameworks/lumenflow/wu-sizing-guide.md.template +2 -2
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +27 -17
- package/templates/core/ai/onboarding/starting-prompt.md.template +2 -1
package/packs/sidekick/README.md
CHANGED
|
@@ -1,194 +1,199 @@
|
|
|
1
1
|
# Sidekick Pack
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Workspace-local productivity pack for the LumenFlow kernel. Sidekick manages tasks, memory,
|
|
4
|
+
channels, routines, and status under `.sidekick/`, while keeping every tool call inside the normal
|
|
5
|
+
kernel scope, policy, and evidence pipeline.
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
**Version**: 0.1.0 (pre-release)
|
|
10
|
-
**License**: AGPL-3.0-only
|
|
9
|
+
**Version**: `0.1.0` (pre-release)
|
|
10
|
+
**License**: `AGPL-3.0-only`
|
|
11
11
|
|
|
12
12
|
## Tool Groups
|
|
13
13
|
|
|
14
|
-
| Group | Tools
|
|
15
|
-
| ------- |
|
|
16
|
-
| Task | `task:create`, `task:list`, `task:complete`, `task:schedule` | Create,
|
|
17
|
-
| Memory | `memory:store`, `memory:recall`, `memory:forget`
|
|
18
|
-
| Channel | `channel:configure`, `channel:send`, `channel:receive`
|
|
19
|
-
| Routine | `routine:create`, `routine:list`, `routine:run`
|
|
20
|
-
| System | `sidekick:init`, `sidekick:status`, `sidekick:export`
|
|
21
|
-
|
|
22
|
-
**Total**: 16 tools
|
|
23
|
-
|
|
24
|
-
## Key Design Decisions
|
|
25
|
-
|
|
26
|
-
- **`.sidekick/` is workspace-local storage.** All data lives under the project
|
|
27
|
-
root in a `.sidekick/` directory managed by the `StoragePort` abstraction.
|
|
28
|
-
- **`routine:run` returns a plan only.** It resolves the routine definition and
|
|
29
|
-
returns the ordered list of steps with their inputs. It does not execute them.
|
|
30
|
-
- **`sidekick:export` is read-only.** It returns all stored data as a JSON
|
|
31
|
-
structure. It does not write files to disk.
|
|
32
|
-
- **Every write tool supports `dry_run`.** When `dry_run: true`, the tool
|
|
33
|
-
validates input and returns what it would do, without persisting changes.
|
|
34
|
-
- **Pack manifest is the contract.** There is no separate contract package. The
|
|
35
|
-
`manifest.yaml` file defines tool names, schemas, scopes, and policies.
|
|
14
|
+
| Group | Tools | Description |
|
|
15
|
+
| ------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
|
|
16
|
+
| Task | `task:create`, `task:list`, `task:update`, `task:cancel`, `task:complete`, `task:schedule` | Create, manage, cancel, complete, and schedule local tasks |
|
|
17
|
+
| Memory | `memory:store`, `memory:recall`, `memory:update`, `memory:forget` | Persist and edit reusable workspace knowledge |
|
|
18
|
+
| Channel | `channel:configure`, `channel:list`, `channel:delete`, `channel:send`, `channel:receive` | Discover and manage named local message channels |
|
|
19
|
+
| Routine | `routine:create`, `routine:list`, `routine:update`, `routine:delete`, `routine:run` | Define, stop, inspect, and remove plan-only routines |
|
|
20
|
+
| System | `sidekick:init`, `sidekick:status`, `sidekick:export` | Bootstrap, health check, and export Sidekick-managed state |
|
|
36
21
|
|
|
37
|
-
|
|
22
|
+
**Total**: 23 tools
|
|
38
23
|
|
|
39
|
-
|
|
24
|
+
## Behavior Notes
|
|
40
25
|
|
|
41
|
-
|
|
26
|
+
- `.sidekick/` is the pack-owned storage root.
|
|
27
|
+
- `routine:run` is plan-only. It returns the resolved steps and never executes them.
|
|
28
|
+
- `routine:update` with `enabled: false` is the stop primitive for routines.
|
|
29
|
+
- `task:cancel` is the destructive task terminal state; it is separate from `task:complete`.
|
|
30
|
+
- `sidekick:export` is read-only. It returns data and does not write export artifacts.
|
|
31
|
+
- All write-capable and destructive tools support `dry_run`.
|
|
32
|
+
- Destructive Sidekick tools are approval-gated through the pack policy factory.
|
|
42
33
|
|
|
43
|
-
-
|
|
44
|
-
- HTTP surface running (for HTTP dispatch tests)
|
|
34
|
+
### Approval-Gated Destructive Tools
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
These tools require policy evaluation to return `approval_required` before execution:
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
- `task:cancel`
|
|
39
|
+
- `memory:forget`
|
|
40
|
+
- `channel:delete`
|
|
41
|
+
- `routine:delete`
|
|
42
|
+
|
|
43
|
+
The permission label helps classify them as destructive, but the actual pause comes from the pack
|
|
44
|
+
policy path and kernel approval flow.
|
|
45
|
+
|
|
46
|
+
## Manual Smoke Flow
|
|
47
|
+
|
|
48
|
+
Use this sequence after installation or whenever you want to smoke test the current contract.
|
|
49
|
+
|
|
50
|
+
### 1. Initialize storage
|
|
51
|
+
|
|
52
|
+
```text
|
|
50
53
|
tool: sidekick:init
|
|
51
54
|
input: {}
|
|
52
55
|
```
|
|
53
56
|
|
|
54
|
-
Expected: `
|
|
57
|
+
Expected: `initialized: true` and a `.sidekick/` root path.
|
|
55
58
|
|
|
56
|
-
###
|
|
59
|
+
### 2. Create and update a task
|
|
57
60
|
|
|
58
|
-
```
|
|
61
|
+
```text
|
|
59
62
|
tool: task:create
|
|
60
63
|
input: { "title": "Review docs", "priority": "P1", "tags": ["docs"] }
|
|
61
64
|
```
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
```text
|
|
67
|
+
tool: task:update
|
|
68
|
+
input: {
|
|
69
|
+
"id": "<task-id>",
|
|
70
|
+
"description": "Check final Sidekick contract",
|
|
71
|
+
"due_at": "2026-03-31T00:00:00Z"
|
|
72
|
+
}
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
Expected:
|
|
75
|
+
Expected: `task:list` returns the updated task metadata.
|
|
73
76
|
|
|
74
|
-
###
|
|
77
|
+
### 3. Store and update memory
|
|
75
78
|
|
|
76
|
-
```
|
|
79
|
+
```text
|
|
77
80
|
tool: memory:store
|
|
78
|
-
input: { "type": "note", "content": "Sidekick
|
|
81
|
+
input: { "type": "note", "content": "Sidekick docs updated", "tags": ["docs"] }
|
|
79
82
|
```
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
tool: memory:recall
|
|
87
|
-
input: { "query": "validated", "type": "note" }
|
|
84
|
+
```text
|
|
85
|
+
tool: memory:update
|
|
86
|
+
input: { "id": "<memory-id>", "type": "snippet", "content": "task:cancel is approval-gated" }
|
|
88
87
|
```
|
|
89
88
|
|
|
90
|
-
Expected:
|
|
89
|
+
Expected: `memory:recall` returns the edited record.
|
|
91
90
|
|
|
92
|
-
###
|
|
91
|
+
### 4. Create and stop a routine
|
|
93
92
|
|
|
94
|
-
```
|
|
93
|
+
```text
|
|
95
94
|
tool: routine:create
|
|
96
95
|
input: {
|
|
97
96
|
"name": "daily-review",
|
|
98
97
|
"steps": [
|
|
99
98
|
{ "tool": "task:list", "input": { "status": "pending" } },
|
|
100
99
|
{ "tool": "sidekick:status", "input": {} }
|
|
101
|
-
]
|
|
100
|
+
],
|
|
101
|
+
"enabled": true
|
|
102
102
|
}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
```text
|
|
106
|
+
tool: routine:update
|
|
107
|
+
input: { "id": "<routine-id>", "enabled": false }
|
|
108
|
+
```
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
Expected: `routine:list` still shows the routine, while `routine:list { "enabled_only": true }`
|
|
111
|
+
excludes it.
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
### 5. Inspect a routine plan
|
|
114
|
+
|
|
115
|
+
```text
|
|
110
116
|
tool: routine:run
|
|
111
|
-
input: { "id": "<routine-id
|
|
117
|
+
input: { "id": "<routine-id>" }
|
|
112
118
|
```
|
|
113
119
|
|
|
114
|
-
Expected: A plan object listing the
|
|
115
|
-
side-effects -- the steps are NOT executed.
|
|
120
|
+
Expected: A plan object listing the ordered steps. No side effects occur.
|
|
116
121
|
|
|
117
|
-
###
|
|
122
|
+
### 6. Check channel discovery
|
|
118
123
|
|
|
119
|
-
```
|
|
120
|
-
tool:
|
|
124
|
+
```text
|
|
125
|
+
tool: channel:send
|
|
126
|
+
input: { "channel": "alerts", "content": "Docs smoke flow" }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
tool: channel:list
|
|
121
131
|
input: {}
|
|
122
132
|
```
|
|
123
133
|
|
|
124
|
-
Expected:
|
|
125
|
-
`
|
|
134
|
+
Expected: A local channel entry with stable metadata such as `message_count` and
|
|
135
|
+
`last_message_at`.
|
|
126
136
|
|
|
127
|
-
###
|
|
137
|
+
### 7. Verify a destructive dry run
|
|
128
138
|
|
|
129
|
-
```
|
|
130
|
-
tool:
|
|
131
|
-
input: { "
|
|
139
|
+
```text
|
|
140
|
+
tool: task:cancel
|
|
141
|
+
input: { "id": "<task-id>", "dry_run": true }
|
|
132
142
|
```
|
|
133
143
|
|
|
134
|
-
Expected:
|
|
144
|
+
Expected: The response previews the canceled task and includes `dry_run: true`, while
|
|
145
|
+
`task:list { "status": "pending" }` still shows the task as pending.
|
|
135
146
|
|
|
136
|
-
###
|
|
147
|
+
### 8. Verify approval-gated destructive behavior
|
|
137
148
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
input: { "id": "<task-id-from-step-2>", "note": "Smoke test passed" }
|
|
141
|
-
```
|
|
149
|
+
Run `task:cancel`, `memory:forget`, `channel:delete`, or `routine:delete` through the kernel
|
|
150
|
+
runtime or host surface that evaluates policies.
|
|
142
151
|
|
|
143
|
-
Expected:
|
|
152
|
+
Expected: The first attempt returns `APPROVAL_REQUIRED`. After approval resolution, the tool runs
|
|
153
|
+
and the audit trail reflects the final delete or cancel transition.
|
|
144
154
|
|
|
145
|
-
###
|
|
155
|
+
### 9. Export
|
|
146
156
|
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-d '{ "context": { "task_id": "smoke-test", "workspace_id": "test" } }'
|
|
157
|
+
```text
|
|
158
|
+
tool: sidekick:export
|
|
159
|
+
input: { "include_audit": true }
|
|
151
160
|
```
|
|
152
161
|
|
|
153
|
-
Expected:
|
|
154
|
-
|
|
155
|
-
### Dry Run Verification
|
|
156
|
-
|
|
157
|
-
Repeat Step 2 with `"dry_run": true` added to the input. Verify that the
|
|
158
|
-
response includes `"dry_run": true` and that `task:list` does not return the
|
|
159
|
-
dry-run task.
|
|
162
|
+
Expected: A full JSON snapshot of stores plus audit events.
|
|
160
163
|
|
|
161
164
|
## Validation
|
|
162
165
|
|
|
163
166
|
```bash
|
|
164
|
-
#
|
|
167
|
+
# Pack contract and integrity
|
|
165
168
|
pnpm pack:validate --id sidekick
|
|
166
169
|
|
|
167
|
-
#
|
|
168
|
-
|
|
170
|
+
# Sidekick tool and manifest tests
|
|
171
|
+
pnpm vitest run packages/@lumenflow/packs/sidekick/__tests__/
|
|
169
172
|
|
|
170
|
-
#
|
|
171
|
-
|
|
173
|
+
# Kernel approval coverage for Sidekick destructive tools
|
|
174
|
+
pnpm vitest run packages/@lumenflow/kernel/src/__tests__/runtime.test.ts
|
|
172
175
|
```
|
|
173
176
|
|
|
174
|
-
## Storage
|
|
177
|
+
## Storage Layout
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
(`FsStoragePort`). Each store (tasks, memories, channels, messages, routines)
|
|
178
|
-
is a JSON array file under `.sidekick/`. An append-only `audit.jsonl` file
|
|
179
|
-
records every tool invocation.
|
|
179
|
+
Sidekick uses a `StoragePort` abstraction with a filesystem adapter. The persisted layout is:
|
|
180
180
|
|
|
181
|
-
```
|
|
181
|
+
```text
|
|
182
182
|
.sidekick/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
channels
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
audit/
|
|
184
|
+
events.jsonl
|
|
185
|
+
channels/
|
|
186
|
+
channels.json
|
|
187
|
+
messages.json
|
|
188
|
+
memory/
|
|
189
|
+
memories.json
|
|
190
|
+
routines/
|
|
191
|
+
routines.json
|
|
192
|
+
tasks/
|
|
193
|
+
tasks.json
|
|
189
194
|
```
|
|
190
195
|
|
|
191
196
|
## Contributing
|
|
192
197
|
|
|
193
|
-
This pack follows the LumenFlow
|
|
194
|
-
|
|
198
|
+
This pack follows the LumenFlow workflow. See the repository `LUMENFLOW.md` and workspace rules for
|
|
199
|
+
delivery expectations.
|
|
@@ -2,23 +2,18 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
|
|
10
|
-
} from './tools/types.js';
|
|
5
|
+
DomainPackManifestSchema,
|
|
6
|
+
POLICY_TRIGGERS,
|
|
7
|
+
type DomainPackManifest,
|
|
8
|
+
type DomainPackTool,
|
|
9
|
+
} from '@lumenflow/kernel';
|
|
10
|
+
import type { PathScope, ToolPermission } from './tools/types.js';
|
|
11
11
|
|
|
12
12
|
interface Parser<T> {
|
|
13
13
|
parse(input: unknown): T;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const MANIFEST_POLICY_TRIGGERS =
|
|
17
|
-
ON_TOOL_REQUEST: 'on_tool_request',
|
|
18
|
-
ON_CLAIM: 'on_claim',
|
|
19
|
-
ON_COMPLETION: 'on_completion',
|
|
20
|
-
ON_EVIDENCE_ADDED: 'on_evidence_added',
|
|
21
|
-
} as const;
|
|
16
|
+
export const MANIFEST_POLICY_TRIGGERS = POLICY_TRIGGERS;
|
|
22
17
|
|
|
23
18
|
export type ManifestPolicyTrigger =
|
|
24
19
|
(typeof MANIFEST_POLICY_TRIGGERS)[keyof typeof MANIFEST_POLICY_TRIGGERS];
|
|
@@ -26,237 +21,29 @@ export type ManifestPolicyTrigger =
|
|
|
26
21
|
export const MANIFEST_POLICY_DECISIONS = {
|
|
27
22
|
ALLOW: 'allow',
|
|
28
23
|
DENY: 'deny',
|
|
24
|
+
APPROVAL_REQUIRED: 'approval_required',
|
|
29
25
|
} as const;
|
|
30
26
|
|
|
31
27
|
export type ManifestPolicyDecision =
|
|
32
28
|
(typeof MANIFEST_POLICY_DECISIONS)[keyof typeof MANIFEST_POLICY_DECISIONS];
|
|
33
29
|
|
|
34
|
-
export interface SidekickManifestTool
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
export interface SidekickManifestTool extends Omit<
|
|
31
|
+
DomainPackTool,
|
|
32
|
+
'permission' | 'required_scopes'
|
|
33
|
+
> {
|
|
37
34
|
permission: ToolPermission;
|
|
38
35
|
required_scopes: PathScope[];
|
|
39
|
-
input_schema?: Record<string, unknown>;
|
|
40
|
-
output_schema?: Record<string, unknown>;
|
|
41
|
-
internal_only?: boolean;
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
export
|
|
45
|
-
id: string;
|
|
46
|
-
trigger: ManifestPolicyTrigger;
|
|
47
|
-
decision: ManifestPolicyDecision;
|
|
48
|
-
reason?: string;
|
|
49
|
-
}
|
|
38
|
+
export type SidekickManifestPolicy = DomainPackManifest['policies'][number];
|
|
50
39
|
|
|
51
|
-
export interface SidekickPackManifest {
|
|
52
|
-
id: string;
|
|
53
|
-
version: string;
|
|
54
|
-
config_key?: string;
|
|
55
|
-
config_schema?: string;
|
|
56
|
-
task_types: string[];
|
|
40
|
+
export interface SidekickPackManifest extends Omit<DomainPackManifest, 'tools' | 'policies'> {
|
|
57
41
|
tools: SidekickManifestTool[];
|
|
58
42
|
policies: SidekickManifestPolicy[];
|
|
59
|
-
evidence_types: string[];
|
|
60
|
-
state_aliases: Record<string, string>;
|
|
61
|
-
lane_templates: Array<{ id: string }>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function asRecord(input: unknown, label: string): Record<string, unknown> {
|
|
65
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
66
|
-
throw new Error(`${label} must be an object.`);
|
|
67
|
-
}
|
|
68
|
-
return input as Record<string, unknown>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseNonEmptyString(value: unknown, label: string): string {
|
|
72
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
73
|
-
throw new Error(`${label} must be a non-empty string.`);
|
|
74
|
-
}
|
|
75
|
-
return value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function parseStringArray(value: unknown, label: string): string[] {
|
|
79
|
-
if (!Array.isArray(value)) {
|
|
80
|
-
throw new Error(`${label} must be an array.`);
|
|
81
|
-
}
|
|
82
|
-
return value.map((entry, index) => parseNonEmptyString(entry, `${label}[${index}]`));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function parseJsonSchemaObject(value: unknown, label: string): Record<string, unknown> {
|
|
86
|
-
return asRecord(value, label);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isSemver(value: string): boolean {
|
|
90
|
-
let core = value;
|
|
91
|
-
const prereleaseIndex = core.indexOf('-');
|
|
92
|
-
if (prereleaseIndex >= 0) {
|
|
93
|
-
core = core.slice(0, prereleaseIndex);
|
|
94
|
-
}
|
|
95
|
-
const metadataIndex = core.indexOf('+');
|
|
96
|
-
if (metadataIndex >= 0) {
|
|
97
|
-
core = core.slice(0, metadataIndex);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const parts = core.split('.');
|
|
101
|
-
if (parts.length !== 3) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return parts.every(
|
|
106
|
-
(part) => part.length > 0 && [...part].every((char) => char >= '0' && char <= '9'),
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const ALLOWED_POLICY_TRIGGERS = new Set<string>(Object.values(MANIFEST_POLICY_TRIGGERS));
|
|
111
|
-
const ALLOWED_POLICY_DECISIONS = new Set<string>(Object.values(MANIFEST_POLICY_DECISIONS));
|
|
112
|
-
const ALLOWED_TOOL_PERMISSIONS = new Set<string>(Object.values(TOOL_PERMISSIONS));
|
|
113
|
-
|
|
114
|
-
function parsePathScope(input: unknown, label: string): PathScope {
|
|
115
|
-
const scope = asRecord(input, label);
|
|
116
|
-
const type = parseNonEmptyString(scope.type, `${label}.type`);
|
|
117
|
-
const pattern = parseNonEmptyString(scope.pattern, `${label}.pattern`);
|
|
118
|
-
const access = parseNonEmptyString(scope.access, `${label}.access`);
|
|
119
|
-
|
|
120
|
-
if (type !== TOOL_SCOPE_TYPES.PATH) {
|
|
121
|
-
throw new Error(`${label}.type must be "${TOOL_SCOPE_TYPES.PATH}".`);
|
|
122
|
-
}
|
|
123
|
-
if (access !== TOOL_SCOPE_ACCESS.READ && access !== TOOL_SCOPE_ACCESS.WRITE) {
|
|
124
|
-
throw new Error(
|
|
125
|
-
`${label}.access must be "${TOOL_SCOPE_ACCESS.READ}" or "${TOOL_SCOPE_ACCESS.WRITE}".`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
type: TOOL_SCOPE_TYPES.PATH,
|
|
131
|
-
pattern,
|
|
132
|
-
access,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function parseRequiredScopes(value: unknown, label: string): PathScope[] {
|
|
137
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
138
|
-
throw new Error(`${label} must be a non-empty array.`);
|
|
139
|
-
}
|
|
140
|
-
return value.map((entry, index) => parsePathScope(entry, `${label}[${index}]`));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function parsePolicy(input: unknown, index: number): SidekickManifestPolicy {
|
|
144
|
-
const policy = asRecord(input, `policies[${index}]`);
|
|
145
|
-
const trigger = parseNonEmptyString(policy.trigger, `policies[${index}].trigger`);
|
|
146
|
-
const decision = parseNonEmptyString(policy.decision, `policies[${index}].decision`);
|
|
147
|
-
|
|
148
|
-
if (!ALLOWED_POLICY_TRIGGERS.has(trigger)) {
|
|
149
|
-
throw new Error(`policies[${index}].trigger is invalid.`);
|
|
150
|
-
}
|
|
151
|
-
if (!ALLOWED_POLICY_DECISIONS.has(decision)) {
|
|
152
|
-
throw new Error(`policies[${index}].decision is invalid.`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
id: parseNonEmptyString(policy.id, `policies[${index}].id`),
|
|
157
|
-
trigger: trigger as ManifestPolicyTrigger,
|
|
158
|
-
decision: decision as ManifestPolicyDecision,
|
|
159
|
-
reason:
|
|
160
|
-
policy.reason === undefined
|
|
161
|
-
? undefined
|
|
162
|
-
: parseNonEmptyString(policy.reason, `policies[${index}].reason`),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function parseTool(input: unknown, index: number): SidekickManifestTool {
|
|
167
|
-
const tool = asRecord(input, `tools[${index}]`);
|
|
168
|
-
const permission =
|
|
169
|
-
tool.permission === undefined
|
|
170
|
-
? TOOL_PERMISSIONS.READ
|
|
171
|
-
: parseNonEmptyString(tool.permission, `tools[${index}].permission`);
|
|
172
|
-
|
|
173
|
-
if (!ALLOWED_TOOL_PERMISSIONS.has(permission)) {
|
|
174
|
-
throw new Error(`tools[${index}].permission is invalid.`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
name: parseNonEmptyString(tool.name, `tools[${index}].name`),
|
|
179
|
-
entry: parseNonEmptyString(tool.entry, `tools[${index}].entry`),
|
|
180
|
-
permission: permission as ToolPermission,
|
|
181
|
-
required_scopes: parseRequiredScopes(tool.required_scopes, `tools[${index}].required_scopes`),
|
|
182
|
-
input_schema:
|
|
183
|
-
tool.input_schema === undefined
|
|
184
|
-
? undefined
|
|
185
|
-
: parseJsonSchemaObject(tool.input_schema, `tools[${index}].input_schema`),
|
|
186
|
-
output_schema:
|
|
187
|
-
tool.output_schema === undefined
|
|
188
|
-
? undefined
|
|
189
|
-
: parseJsonSchemaObject(tool.output_schema, `tools[${index}].output_schema`),
|
|
190
|
-
internal_only:
|
|
191
|
-
tool.internal_only === undefined
|
|
192
|
-
? undefined
|
|
193
|
-
: (() => {
|
|
194
|
-
if (typeof tool.internal_only !== 'boolean') {
|
|
195
|
-
throw new Error(`tools[${index}].internal_only must be boolean.`);
|
|
196
|
-
}
|
|
197
|
-
return tool.internal_only;
|
|
198
|
-
})(),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function parseStateAliases(input: unknown): Record<string, string> {
|
|
203
|
-
const aliases = asRecord(input ?? {}, 'state_aliases');
|
|
204
|
-
const parsed: Record<string, string> = {};
|
|
205
|
-
for (const [key, value] of Object.entries(aliases)) {
|
|
206
|
-
parsed[parseNonEmptyString(key, 'state_aliases key')] = parseNonEmptyString(
|
|
207
|
-
value,
|
|
208
|
-
`state_aliases.${key}`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
return parsed;
|
|
212
43
|
}
|
|
213
44
|
|
|
214
45
|
export const SidekickManifestSchema: Parser<SidekickPackManifest> = {
|
|
215
46
|
parse(input: unknown): SidekickPackManifest {
|
|
216
|
-
|
|
217
|
-
const version = parseNonEmptyString(manifest.version, 'version');
|
|
218
|
-
if (!isSemver(version)) {
|
|
219
|
-
throw new Error('version must be semver.');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const taskTypes = parseStringArray(manifest.task_types, 'task_types');
|
|
223
|
-
if (taskTypes.length === 0) {
|
|
224
|
-
throw new Error('task_types must include at least one item.');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const toolsValue = manifest.tools ?? [];
|
|
228
|
-
if (!Array.isArray(toolsValue)) {
|
|
229
|
-
throw new Error('tools must be an array.');
|
|
230
|
-
}
|
|
231
|
-
const policiesValue = manifest.policies ?? [];
|
|
232
|
-
if (!Array.isArray(policiesValue)) {
|
|
233
|
-
throw new Error('policies must be an array.');
|
|
234
|
-
}
|
|
235
|
-
const laneTemplatesValue = manifest.lane_templates ?? [];
|
|
236
|
-
if (!Array.isArray(laneTemplatesValue)) {
|
|
237
|
-
throw new Error('lane_templates must be an array.');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
id: parseNonEmptyString(manifest.id, 'id'),
|
|
242
|
-
version,
|
|
243
|
-
config_key:
|
|
244
|
-
manifest.config_key === undefined
|
|
245
|
-
? undefined
|
|
246
|
-
: parseNonEmptyString(manifest.config_key, 'config_key'),
|
|
247
|
-
config_schema:
|
|
248
|
-
manifest.config_schema === undefined
|
|
249
|
-
? undefined
|
|
250
|
-
: parseNonEmptyString(manifest.config_schema, 'config_schema'),
|
|
251
|
-
task_types: taskTypes,
|
|
252
|
-
tools: toolsValue.map((tool, index) => parseTool(tool, index)),
|
|
253
|
-
policies: policiesValue.map((policy, index) => parsePolicy(policy, index)),
|
|
254
|
-
evidence_types: parseStringArray(manifest.evidence_types ?? [], 'evidence_types'),
|
|
255
|
-
state_aliases: parseStateAliases(manifest.state_aliases),
|
|
256
|
-
lane_templates: laneTemplatesValue.map((laneTemplate, index) => {
|
|
257
|
-
const entry = asRecord(laneTemplate, `lane_templates[${index}]`);
|
|
258
|
-
return { id: parseNonEmptyString(entry.id, `lane_templates[${index}].id`) };
|
|
259
|
-
}),
|
|
260
|
-
};
|
|
47
|
+
return DomainPackManifestSchema.parse(input) as SidekickPackManifest;
|
|
261
48
|
},
|
|
262
49
|
};
|