oh-my-llmwikimode 1.5.0 → 1.7.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/docs/KNOWLEDGE_FOLDER_TAXONOMY.md +1 -1
- package/docs/KNOWLEDGE_UX_DASHBOARD_CONTRACT.md +1 -1
- package/docs/design/EOS_CHAT_HOME_IA.md +22 -0
- package/docs/design/EOS_DESIGN.md +66 -0
- package/docs/eos-approval-handoff-workbench.md +76 -0
- package/package.json +3 -1
- package/src/eos/approval/artifacts.js +164 -0
- package/src/eos/approval/contract.js +165 -0
- package/src/eos/approval/handoff-contract.js +200 -0
- package/src/eos/approval/handoff-read-model.js +80 -0
- package/src/eos/approval/handoffs.js +245 -0
- package/src/eos/approval/read-model.js +82 -0
- package/src/eos/artifact-inspector/view-model.js +245 -0
- package/src/eos/home-review-queue.js +155 -0
- package/src/eos/home-workspace.js +88 -0
- package/src/eos/home.js +4 -203
- package/src/eos/http/app.js +2 -1
- package/src/eos/http/routes/home.js +60 -0
- package/src/eos/http/routes/side-review.js +51 -0
- package/src/eos/timeline-status/read-model.js +281 -0
- package/src/eos/workspace/static/app.js +178 -1
- package/src/eos/workspace/static/index.html +33 -3
- package/src/eos/workspace/static/styles.css +7 -0
- package/src/wiki/save-routing.js +4 -3
|
@@ -25,7 +25,7 @@ Example:
|
|
|
25
25
|
| Current folder | Canonical label | Purpose |
|
|
26
26
|
| --- | --- | --- |
|
|
27
27
|
| `!대시보드` | `!대시보드_dashboard` | User-facing entry point and daily summary |
|
|
28
|
-
| `0_omx 핸드오프` | `
|
|
28
|
+
| `0_omx 핸드오프` | `0_omw-handoff_omw-handoff` | OMW / Codex handoff plans and run instructions; old OMX folder names remain legacy readable inputs |
|
|
29
29
|
| `개발-진행-기록` | `개발-진행-기록_dev-history` | Work logs, roadmap updates, implementation and release history |
|
|
30
30
|
| `개발-진행-기록/로드맵` | `개발-진행-기록_dev-history/로드맵_roadmap` | Roadmaps and planning timeline |
|
|
31
31
|
| `개발-진행-기록/구현-보고` | `개발-진행-기록_dev-history/구현-보고_implementation-reports` | Implementation reports |
|
|
@@ -22,7 +22,7 @@ Snapshot source: local wiki root `C:\Users\USER\Documents\llm-wiki`.
|
|
|
22
22
|
- Markdown entries under the project area: about 177
|
|
23
23
|
- Current top-level project folders:
|
|
24
24
|
- `!대시보드`
|
|
25
|
-
- `0_omx 핸드오프`
|
|
25
|
+
- `0_omx 핸드오프` (legacy readable input; new handoff output uses `0_omw-handoff_omw-handoff`)
|
|
26
26
|
- `가이드-및-정책`
|
|
27
27
|
- `개발-진행-기록`
|
|
28
28
|
- `아카이브`
|
|
@@ -101,6 +101,9 @@ Do not use these labels in the Eos Chat MVP:
|
|
|
101
101
|
- `GET /workspace` redirects to `/workspace/`.
|
|
102
102
|
- `GET /side-review/` serves the review desk for local artifacts, evidence, and prepare-only decision notes.
|
|
103
103
|
- `GET /api/home` remains a read-only home data snapshot.
|
|
104
|
+
- `POST /api/agent/side-review/approval-requests` prepares a review-required approval request from an existing `prepare_approval` Side Review decision artifact only.
|
|
105
|
+
- `GET /api/workspace/artifacts/inspect` returns whitelisted read-only artifact inspector data by `{type,id}` only.
|
|
106
|
+
- `GET /api/workspace/timeline-status` returns a local lifecycle timeline/status panel derived from EOS local artifacts only.
|
|
104
107
|
- `POST /api/chat/requests` creates a review-required local artifact only.
|
|
105
108
|
|
|
106
109
|
## 12. Browser-first Workspace Slice
|
|
@@ -166,3 +169,22 @@ Stable fields:
|
|
|
166
169
|
- `review_queue.side_review_decision_recent`: latest prepared decision artifacts for the right inspector/Home summary
|
|
167
170
|
|
|
168
171
|
The snapshot is a read model only. It must not become an execution queue, approval queue, hosted sync contract, or source wiki mutation path.
|
|
172
|
+
|
|
173
|
+
## 14. Approval Inspector workspace slice - 2026-06-01
|
|
174
|
+
|
|
175
|
+
`EOS Workspace` now includes an Approval Queue and Artifact Inspector for review-required approval request artifacts.
|
|
176
|
+
|
|
177
|
+
Stable surfaces:
|
|
178
|
+
|
|
179
|
+
- `review_queue.approval_request_recent`: latest local approval request artifacts visible in Home/Workspace.
|
|
180
|
+
- `approval_surface`: local approval queue metadata and route hints for the browser workbench.
|
|
181
|
+
- `/api/agent/side-review/approval-requests`: prepare-only route from `prepare_approval` Side Review decisions to approval request artifacts.
|
|
182
|
+
- `/api/workspace/artifacts/inspect`: safe inspector route for whitelisted artifact types and ids.
|
|
183
|
+
- `/api/workspace/timeline-status`: local lifecycle timeline and Timeline Gate status.
|
|
184
|
+
- Workspace regions: `Approval Queue`, `Artifact Inspector`, `Lifecycle Timeline`, and `Timeline Gate`.
|
|
185
|
+
|
|
186
|
+
Safety contract:
|
|
187
|
+
|
|
188
|
+
- Approval Queue is a review queue, not an execution queue.
|
|
189
|
+
- Artifact Inspector must reject raw local paths, traversal, unsupported types, symlinks, oversized artifacts, and malformed JSON without leaking secrets or user-home paths.
|
|
190
|
+
- Timeline Gate may show `approved_not_executed`, but this is a review state only. It must not imply shell, git, provider, Discord, sync, publish, merge, push, or source wiki mutation work has happened.
|
|
@@ -339,3 +339,69 @@ Verification anchors:
|
|
|
339
339
|
- `test/eos/uiux-product-shape.test.js`
|
|
340
340
|
- `npm run test:eos`
|
|
341
341
|
- `npm run verify:eos-workspace-completion`
|
|
342
|
+
|
|
343
|
+
## Implemented EOS Workspace Approval Inspector Slice - 2026-06-01
|
|
344
|
+
|
|
345
|
+
The EOS Workspace Approval Inspector slice makes approval requests first-class local review artifacts inside the browser-first workbench.
|
|
346
|
+
|
|
347
|
+
What changed:
|
|
348
|
+
|
|
349
|
+
- Approval request artifacts now live under `.system/eos/approval/requests/` with review-required lifecycle statuses, deterministic duplicate handling, and append-only audit metadata.
|
|
350
|
+
- `GET /api/home` includes a local approval queue summary with recent approval request artifacts.
|
|
351
|
+
- `POST /api/agent/side-review/approval-requests` prepares an approval request only from an existing `prepare_approval` Side Review decision artifact.
|
|
352
|
+
- `GET /api/workspace/artifacts/inspect` exposes a whitelisted, read-only artifact inspector for approval requests and adjacent EOS artifacts.
|
|
353
|
+
- `GET /api/workspace/timeline-status` exposes a local-only lifecycle timeline/status panel derived from chat, Side Review, approval, visual QA, and local evidence artifacts.
|
|
354
|
+
- `/workspace/` renders Approval Queue, Artifact Inspector, Lifecycle Timeline, and Timeline Gate surfaces with Local Only, Review Required, and Not Applied safety labels.
|
|
355
|
+
- Browser product evidence is captured under `.omo/ulw-loop/evidence/eos-approval-inspector-20260601/`.
|
|
356
|
+
|
|
357
|
+
Boundary:
|
|
358
|
+
|
|
359
|
+
- The slice remains web-service first, not a VS Code extension or VSIX.
|
|
360
|
+
- Approval statuses such as `approved_not_executed` mean explicit review state only; they do not execute shell, git, provider, Discord, or wiki mutation actions.
|
|
361
|
+
- Side Review approval request creation is prepare-only; it creates a local review artifact and never applies the underlying decision.
|
|
362
|
+
- The inspector reads only whitelisted local artifacts by type/id, rejects raw paths and traversal, redacts secrets and user-home paths, and degrades safely on malformed artifacts.
|
|
363
|
+
- This slice does not add hosted sync, production Discord control, marketplace/account/payment features, package splitting, npm publish, auto-apply, auto-promotion, auto-merge/delete, auto-push, hidden execution, or source wiki mutation.
|
|
364
|
+
|
|
365
|
+
Verification anchors:
|
|
366
|
+
|
|
367
|
+
- `test/eos/approval-request-artifacts.test.js`
|
|
368
|
+
- `test/eos/approval-queue-read-model.test.js`
|
|
369
|
+
- `test/eos/side-review-approval-requests.test.js`
|
|
370
|
+
- `test/eos/artifact-inspector-view-model.test.js`
|
|
371
|
+
- `test/eos/workspace-approval-inspector-routes.test.js`
|
|
372
|
+
- `test/eos/timeline-status-panel.test.js`
|
|
373
|
+
- `test/eos/approval-inspector-gate.test.js`
|
|
374
|
+
- `npm run verify:eos-approval-inspector`
|
|
375
|
+
|
|
376
|
+
## Implemented EOS Approval Handoff Workbench Slice - 2026-06-02
|
|
377
|
+
|
|
378
|
+
The EOS Approval Handoff Workbench slice adds the first safe step after an
|
|
379
|
+
approval request reaches `approved_not_executed`.
|
|
380
|
+
|
|
381
|
+
What changed:
|
|
382
|
+
|
|
383
|
+
- EOS can create a local review-required handoff package under
|
|
384
|
+
`.system/eos/approval/handoffs/`.
|
|
385
|
+
- Handoff package audit records are appended to
|
|
386
|
+
`.system/eos/approval/handoff-audit.jsonl`.
|
|
387
|
+
- The source approval request transitions to `handoff_prepared` only after the
|
|
388
|
+
package artifact exists.
|
|
389
|
+
- `GET /api/home` includes a Handoff Queue summary.
|
|
390
|
+
- `GET /api/workspace/artifacts/inspect?type=approval_handoff&id=<id>` exposes
|
|
391
|
+
a redacted, whitelisted package inspector view.
|
|
392
|
+
- `/workspace/` renders Handoff Queue and package inspection controls using
|
|
393
|
+
package/review language.
|
|
394
|
+
|
|
395
|
+
Boundary:
|
|
396
|
+
|
|
397
|
+
- Handoff packages are human review packages, not external-worker dispatches.
|
|
398
|
+
- This slice does not add shell, git, provider, Discord, worker, queue,
|
|
399
|
+
source-wiki mutation, hosted sync, auto-merge/delete/push/publish, or hidden
|
|
400
|
+
execution behavior.
|
|
401
|
+
|
|
402
|
+
Verification anchors:
|
|
403
|
+
|
|
404
|
+
- `test/eos/approval-handoff-artifacts.test.js`
|
|
405
|
+
- `test/eos/approval-handoff-routes.test.js`
|
|
406
|
+
- `test/eos/workspace-home-routes.test.js`
|
|
407
|
+
- `npm run verify:eos-approval-handoff`
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# EOS Approval Handoff Workbench
|
|
2
|
+
|
|
3
|
+
Status: implemented local product slice.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
EOS can prepare a local handoff package from an approval request that is already
|
|
8
|
+
`approved_not_executed`.
|
|
9
|
+
|
|
10
|
+
The package is written under:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
.system/eos/approval/handoffs/
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Audit records are appended under:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
.system/eos/approval/handoff-audit.jsonl
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The source approval request is updated to `handoff_prepared` only after the
|
|
23
|
+
handoff package artifact exists.
|
|
24
|
+
|
|
25
|
+
## Browser Surface
|
|
26
|
+
|
|
27
|
+
Open the local daemon and visit:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
http://127.0.0.1:4825/workspace/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The Workspace shows:
|
|
34
|
+
|
|
35
|
+
- Approval Queue
|
|
36
|
+
- Handoff Queue
|
|
37
|
+
- Artifact Inspector
|
|
38
|
+
- Lifecycle Timeline
|
|
39
|
+
- Local Only / Review Required / Not Applied safety labels
|
|
40
|
+
|
|
41
|
+
## API Surface
|
|
42
|
+
|
|
43
|
+
Create a package from a local approved request:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
POST /api/workspace/approval-handoffs
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Inspect a package:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
GET /api/workspace/artifacts/inspect?type=approval_handoff&id=<id>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Read current queues through Home:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
GET /api/home
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Safety Boundary
|
|
62
|
+
|
|
63
|
+
Handoff packages are review-required local artifacts only.
|
|
64
|
+
|
|
65
|
+
This slice does not execute shell, git, providers, Discord, external workers,
|
|
66
|
+
queues, wiki source mutation, merge, publish, push, hosted sync, or hidden
|
|
67
|
+
agents. It also does not reuse external-worker dispatch as the primary model.
|
|
68
|
+
|
|
69
|
+
## Verification
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
npm run verify:eos-approval-handoff
|
|
73
|
+
npm run test:eos
|
|
74
|
+
npm run verify:eos-boundary-policy
|
|
75
|
+
npm run verify:eos-boundary
|
|
76
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-llmwikimode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "LLM Wiki plugin for OpenCode ??auto-memory, retrieval, and knowledge promotion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"test:eos": "node --test test/eos/**/*.test.js",
|
|
31
31
|
"verify:eos-workspace-product": "npm run test:eos && npm run verify:eos-boundary-policy && npm run verify:eos-boundary && npm run eos:a11y-snapshot -- --json",
|
|
32
32
|
"verify:eos-workspace-completion": "node scripts/verify-eos-workspace-completion.js",
|
|
33
|
+
"verify:eos-approval-inspector": "node scripts/verify-eos-approval-inspector.js",
|
|
34
|
+
"verify:eos-approval-handoff": "node scripts/verify-eos-approval-handoff.js",
|
|
33
35
|
"verify:package": "node test/verify-package.test.js",
|
|
34
36
|
"verify:docs": "node test/verify-docs.test.js",
|
|
35
37
|
"verify:side-effects": "node scripts/side-effect-audit.js",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
APPROVAL_REQUEST_STATUSES,
|
|
6
|
+
createApprovalAuditEntry,
|
|
7
|
+
createApprovalRequestArtifact,
|
|
8
|
+
validateApprovalRequest,
|
|
9
|
+
} from "./contract.js";
|
|
10
|
+
|
|
11
|
+
export { APPROVAL_REQUEST_STATUSES } from "./contract.js";
|
|
12
|
+
|
|
13
|
+
const AUDIT_ACTIONS = Object.freeze([
|
|
14
|
+
"approval_requested",
|
|
15
|
+
"reject_approval_request",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function resolveUnderWikiRoot(wikiRoot, relativePath) {
|
|
19
|
+
const root = path.resolve(wikiRoot);
|
|
20
|
+
const target = path.resolve(root, ...relativePath.split("/"));
|
|
21
|
+
const relative = path.relative(root, target);
|
|
22
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
23
|
+
throw new Error("Eos approval artifact path escapes wiki root");
|
|
24
|
+
}
|
|
25
|
+
return target;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveRequestArtifactPath(wikiRoot, relativePath) {
|
|
29
|
+
const target = resolveUnderWikiRoot(wikiRoot, relativePath);
|
|
30
|
+
const requestsRoot = path.resolve(getApprovalArtifactPaths(wikiRoot).requests);
|
|
31
|
+
const relative = path.relative(requestsRoot, target);
|
|
32
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
33
|
+
throw new Error("Eos approval artifact path escapes requests directory");
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getApprovalArtifactPaths(wikiRoot) {
|
|
39
|
+
const root = path.join(wikiRoot, ".system", "eos", "approval");
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
root,
|
|
42
|
+
requests: path.join(root, "requests"),
|
|
43
|
+
auditFile: path.join(root, "audit.jsonl"),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function ensureApprovalArtifactStructure(wikiRoot) {
|
|
48
|
+
const paths = getApprovalArtifactPaths(wikiRoot);
|
|
49
|
+
await mkdir(paths.requests, { recursive: true });
|
|
50
|
+
return paths;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function prepareApprovalRequestArtifact(wikiRoot, input = {}, options = {}) {
|
|
54
|
+
if (!wikiRoot || typeof wikiRoot !== "string") {
|
|
55
|
+
throw new Error("wikiRoot is required for Eos approval artifacts");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const now = options.now instanceof Date ? options.now : new Date(options.now || Date.now());
|
|
59
|
+
const validation = validateApprovalRequest(input);
|
|
60
|
+
if (!validation.valid) {
|
|
61
|
+
await appendApprovalAudit(wikiRoot, "reject_approval_request", {
|
|
62
|
+
source_decision_id: input.source_decision_id || "",
|
|
63
|
+
requested_action: input.requested_action || "",
|
|
64
|
+
reason: validation.errors.join("; "),
|
|
65
|
+
}, { ...options, now });
|
|
66
|
+
return rejected(validation.errors.join("; "));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const prepared = createApprovalRequestArtifact(validation.value, { ...options, now });
|
|
70
|
+
const resolvedArtifactPath = resolveRequestArtifactPath(wikiRoot, prepared.artifactPath);
|
|
71
|
+
const existing = await readExistingJson(resolvedArtifactPath);
|
|
72
|
+
if (existing.exists) {
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
success: true,
|
|
75
|
+
duplicate: true,
|
|
76
|
+
request: existing.value,
|
|
77
|
+
artifactPath: prepared.artifactPath,
|
|
78
|
+
executed: false,
|
|
79
|
+
sourceEntryMutated: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await ensureApprovalArtifactStructure(wikiRoot);
|
|
84
|
+
await writeFile(resolvedArtifactPath, `${JSON.stringify(prepared.artifact, null, 2)}\n`, {
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
flag: "wx",
|
|
87
|
+
});
|
|
88
|
+
await appendApprovalAuditEntry(wikiRoot, prepared.auditEntry);
|
|
89
|
+
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
success: true,
|
|
92
|
+
duplicate: false,
|
|
93
|
+
request: prepared.artifact,
|
|
94
|
+
artifactPath: prepared.artifactPath,
|
|
95
|
+
audit: prepared.auditEntry,
|
|
96
|
+
auditPath: ".system/eos/approval/audit.jsonl",
|
|
97
|
+
executed: false,
|
|
98
|
+
sourceEntryMutated: false,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function readApprovalRequestArtifacts(wikiRoot) {
|
|
103
|
+
const paths = getApprovalArtifactPaths(wikiRoot);
|
|
104
|
+
const requests = [];
|
|
105
|
+
const load_errors = [];
|
|
106
|
+
try {
|
|
107
|
+
const entries = await readdir(paths.requests, { withFileTypes: true });
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
110
|
+
const filePath = path.join(paths.requests, entry.name);
|
|
111
|
+
try {
|
|
112
|
+
const artifact = JSON.parse(await readFile(filePath, "utf-8"));
|
|
113
|
+
if (artifact?.type === "eos_approval_request" && APPROVAL_REQUEST_STATUSES.includes(artifact.status)) {
|
|
114
|
+
requests.push(artifact);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
load_errors.push({ path: filePath, error: error instanceof Error ? error.message : String(error) });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error?.code !== "ENOENT") throw error;
|
|
122
|
+
}
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
paths,
|
|
125
|
+
requests: Object.freeze(requests.sort((left, right) => String(left.id).localeCompare(String(right.id)))),
|
|
126
|
+
load_errors: Object.freeze(load_errors),
|
|
127
|
+
summary: Object.freeze({ requests: requests.length, load_errors: load_errors.length }),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readExistingJson(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
return Object.freeze({ exists: true, value: JSON.parse(await readFile(filePath, "utf-8")) });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error?.code === "ENOENT") return Object.freeze({ exists: false, value: null });
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function appendApprovalAudit(wikiRoot, action, entry = {}, options = {}) {
|
|
141
|
+
const payload = {
|
|
142
|
+
...createApprovalAuditEntry(action, entry, options),
|
|
143
|
+
reason: entry.reason || "",
|
|
144
|
+
};
|
|
145
|
+
await appendApprovalAuditEntry(wikiRoot, payload);
|
|
146
|
+
return payload;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function appendApprovalAuditEntry(wikiRoot, entry) {
|
|
150
|
+
if (!AUDIT_ACTIONS.includes(entry.action)) {
|
|
151
|
+
throw new Error(`Unsupported Eos approval audit action: ${entry.action}`);
|
|
152
|
+
}
|
|
153
|
+
const paths = await ensureApprovalArtifactStructure(wikiRoot);
|
|
154
|
+
await appendFile(paths.auditFile, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rejected(error) {
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
success: false,
|
|
160
|
+
error,
|
|
161
|
+
executed: false,
|
|
162
|
+
sourceEntryMutated: false,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const APPROVAL_REQUEST_STATUSES = Object.freeze([
|
|
5
|
+
"review_required",
|
|
6
|
+
"blocked",
|
|
7
|
+
"approved_not_executed",
|
|
8
|
+
"handoff_prepared",
|
|
9
|
+
"rejected",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const APPROVAL_ACTION = "prepare_handoff";
|
|
13
|
+
|
|
14
|
+
const SUPPORTED_SOURCE_ROOTS = Object.freeze([
|
|
15
|
+
".system/eos/chat/requests/",
|
|
16
|
+
".system/eos/side-review/decisions/",
|
|
17
|
+
".system/eos/approval/requests/",
|
|
18
|
+
".system/eos/visual-qa/",
|
|
19
|
+
".omo/ulw-loop/evidence/",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function iso(value) {
|
|
27
|
+
return (value instanceof Date ? value : new Date(value || Date.now())).toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stableJson(value) {
|
|
31
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
32
|
+
if (isRecord(value)) {
|
|
33
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shortHash(value) {
|
|
39
|
+
return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 12);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeId(value, fallback = "approval") {
|
|
43
|
+
return String(value || fallback)
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
46
|
+
.replace(/^-+|-+$/g, "")
|
|
47
|
+
.slice(0, 80) || fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function relativeRequestPath(id) {
|
|
51
|
+
return path.posix.join(".system", "eos", "approval", "requests", `${id}.json`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeRelativePath(value, field, options = {}) {
|
|
55
|
+
if (typeof value !== "string" || !value.trim()) return { error: `${field} is required` };
|
|
56
|
+
const normalized = value.trim().replaceAll("\\", "/");
|
|
57
|
+
if (path.isAbsolute(normalized) || normalized.includes("://")) return { error: `${field} must be a safe relative path` };
|
|
58
|
+
const collapsed = path.posix.normalize(normalized);
|
|
59
|
+
if (collapsed.startsWith("../") || collapsed === ".." || collapsed.startsWith("/")) {
|
|
60
|
+
return { error: `${field} must stay inside the wiki root` };
|
|
61
|
+
}
|
|
62
|
+
if (options.supportedRoot && !SUPPORTED_SOURCE_ROOTS.some((root) => collapsed.startsWith(root))) {
|
|
63
|
+
return { error: `${field} must use a supported approval source root` };
|
|
64
|
+
}
|
|
65
|
+
return { value: collapsed };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateApprovalRequest(input = {}) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
const sourceDecisionId = typeof input.source_decision_id === "string" ? input.source_decision_id.trim() : "";
|
|
71
|
+
const requestedAction = typeof input.requested_action === "string" ? input.requested_action.trim() : "";
|
|
72
|
+
const sourceArtifactPath = normalizeRelativePath(input.source_artifact_path, "source_artifact_path", { supportedRoot: true });
|
|
73
|
+
const sourcePath = input.source_path ? normalizeRelativePath(input.source_path, "source_path") : { value: null };
|
|
74
|
+
const evidence = Array.isArray(input.evidence) ? input.evidence : [];
|
|
75
|
+
|
|
76
|
+
if (!sourceDecisionId) errors.push("source_decision_id is required");
|
|
77
|
+
if (requestedAction !== APPROVAL_ACTION) errors.push(`requested_action must be ${APPROVAL_ACTION}`);
|
|
78
|
+
if (input.status && !APPROVAL_REQUEST_STATUSES.includes(input.status)) errors.push("status is not supported");
|
|
79
|
+
if (sourceArtifactPath.error) errors.push(sourceArtifactPath.error);
|
|
80
|
+
if (sourcePath.error) errors.push(sourcePath.error);
|
|
81
|
+
if (evidence.length === 0) errors.push("evidence is required");
|
|
82
|
+
|
|
83
|
+
if (errors.length > 0) return Object.freeze({ valid: false, errors: Object.freeze(errors) });
|
|
84
|
+
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
valid: true,
|
|
87
|
+
value: Object.freeze({
|
|
88
|
+
source_decision_id: sourceDecisionId,
|
|
89
|
+
source_artifact_path: sourceArtifactPath.value,
|
|
90
|
+
requested_action: requestedAction,
|
|
91
|
+
reason: typeof input.reason === "string" ? input.reason.trim() : "",
|
|
92
|
+
source_path: sourcePath.value,
|
|
93
|
+
evidence: Object.freeze(evidence.map((item) => String(item).trim()).filter(Boolean)),
|
|
94
|
+
requested_by: typeof input.requested_by === "string" && input.requested_by.trim()
|
|
95
|
+
? input.requested_by.trim()
|
|
96
|
+
: "eos-approval",
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createApprovalRequestArtifact(request, options = {}) {
|
|
102
|
+
const seed = {
|
|
103
|
+
source_decision_id: request.source_decision_id,
|
|
104
|
+
source_artifact_path: request.source_artifact_path,
|
|
105
|
+
requested_action: request.requested_action,
|
|
106
|
+
evidence: request.evidence,
|
|
107
|
+
};
|
|
108
|
+
const id = `approval-${safeId(request.source_decision_id)}-${safeId(request.requested_action)}-${shortHash(seed)}`;
|
|
109
|
+
const artifactPath = relativeRequestPath(id);
|
|
110
|
+
const auditEntry = createApprovalAuditEntry("approval_requested", request, options, { requestId: id, artifactPath });
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
artifactPath,
|
|
113
|
+
artifact: Object.freeze({
|
|
114
|
+
schema_version: 1,
|
|
115
|
+
type: "eos_approval_request",
|
|
116
|
+
id,
|
|
117
|
+
source_decision_id: request.source_decision_id,
|
|
118
|
+
source_artifact_path: request.source_artifact_path,
|
|
119
|
+
requested_action: request.requested_action,
|
|
120
|
+
reason: request.reason,
|
|
121
|
+
source_path: request.source_path,
|
|
122
|
+
evidence: request.evidence,
|
|
123
|
+
status: "review_required",
|
|
124
|
+
requested_by: request.requested_by,
|
|
125
|
+
safety_flags: Object.freeze({
|
|
126
|
+
review_required: true,
|
|
127
|
+
approved_not_executed: false,
|
|
128
|
+
executed: false,
|
|
129
|
+
source_entry_mutated: false,
|
|
130
|
+
}),
|
|
131
|
+
boundaries: Object.freeze({
|
|
132
|
+
no_hidden_execution: true,
|
|
133
|
+
no_source_mutation: true,
|
|
134
|
+
auto_apply: false,
|
|
135
|
+
auto_promote: false,
|
|
136
|
+
auto_merge: false,
|
|
137
|
+
auto_delete: false,
|
|
138
|
+
auto_push: false,
|
|
139
|
+
auto_publish: false,
|
|
140
|
+
hosted_sync: false,
|
|
141
|
+
}),
|
|
142
|
+
audit: Object.freeze([auditEntry]),
|
|
143
|
+
created_at: iso(options.now),
|
|
144
|
+
updated_at: iso(options.now),
|
|
145
|
+
artifact_path: artifactPath,
|
|
146
|
+
}),
|
|
147
|
+
auditEntry,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createApprovalAuditEntry(action, request, options = {}, refs = {}) {
|
|
152
|
+
return Object.freeze({
|
|
153
|
+
schema_version: 1,
|
|
154
|
+
action,
|
|
155
|
+
recorded_at: iso(options.now),
|
|
156
|
+
actor: options.actor || "eos-approval",
|
|
157
|
+
request_id: refs.requestId || null,
|
|
158
|
+
artifact_path: refs.artifactPath || null,
|
|
159
|
+
source_decision_id: request.source_decision_id || "",
|
|
160
|
+
requested_action: request.requested_action || "",
|
|
161
|
+
executed: false,
|
|
162
|
+
live_execution: false,
|
|
163
|
+
source_entry_mutated: false,
|
|
164
|
+
});
|
|
165
|
+
}
|