chainlesschain 0.166.140 → 0.166.142
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 +161 -13
- package/package.json +2 -2
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/goal.js +161 -0
- package/src/data/changelog.json +26 -0
- package/src/lib/evolution/evolution-artifact-ports.js +64 -1
- package/src/lib/plugin-runtime/cli-goal-capabilities.js +28 -0
- package/src/lib/plugin-runtime/cli-goal-draft-request.js +2 -6
- package/src/lib/plugin-runtime/cli-goal-drafts.js +2 -6
- package/src/lib/plugin-runtime/cli-goal-event-history.js +284 -0
- package/src/lib/plugin-runtime/cli-goal-events.js +175 -0
- package/src/lib/plugin-runtime/cli-goal-extension-host.js +21 -13
- package/src/lib/plugin-runtime/cli-project-goal-events.js +212 -0
- package/src/lib/plugin-runtime/examples/goal-project-observer/plugin.json +20 -0
- package/src/lib/plugin-runtime/examples/goal-status-observer/plugin.json +17 -0
- package/src/lib/plugin-runtime/examples/project-delivery-assistant/README.md +92 -0
- package/src/lib/plugin-runtime/examples/project-delivery-assistant/plugin.json +6 -7
- package/src/lib/plugin-runtime/goal-extensions.js +16 -0
- package/src/lib/project-goal-connection.js +111 -27
- package/src/lib/read-file-loop-guard.js +46 -11
- package/src/lib/tool-result-preview.js +1 -0
- package/src/runtime/agent-core.js +58 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/** A separately consented v4 lease observes bounded native evidence snapshots. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { performance } from "node:perf_hooks";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { cliGoalExtensionHost } from "./cli-goal-capabilities.js";
|
|
7
|
+
import { managedSettingsPath } from "../settings-loader.cjs";
|
|
8
|
+
import { createProjectGoalConnection } from "../project-goal-connection.js";
|
|
9
|
+
import {
|
|
10
|
+
collectGoalExtensions,
|
|
11
|
+
projectGoalExtensionEvent,
|
|
12
|
+
} from "./goal-extensions.js";
|
|
13
|
+
|
|
14
|
+
const TYPES = ["risk.review.completed", "action.receipt.updated"];
|
|
15
|
+
const SCHEMA = "chainlesschain.goal-extension-stream/v1";
|
|
16
|
+
export const _deps = { createProjectGoalConnection, projectGoalExtensionEvent };
|
|
17
|
+
const fail = (code) => {
|
|
18
|
+
throw new Error(code);
|
|
19
|
+
};
|
|
20
|
+
const digest = (value) =>
|
|
21
|
+
createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
22
|
+
|
|
23
|
+
/** Capture options before any I/O. Never reinterpret another lease as events. */
|
|
24
|
+
export function watchCliProjectGoalEvents(id, options = {}) {
|
|
25
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
26
|
+
const env = Object.freeze({ ...(options.env || process.env) });
|
|
27
|
+
const managedSettingsFile = managedSettingsPath({
|
|
28
|
+
env,
|
|
29
|
+
managedSettingsFile: options.managedSettingsFile,
|
|
30
|
+
});
|
|
31
|
+
const name = options.plugin,
|
|
32
|
+
scope = options.scope,
|
|
33
|
+
expectedStore = options.expectedStore;
|
|
34
|
+
const connectionFile = options.connection;
|
|
35
|
+
const intervalMs = options.intervalMs ?? 1000,
|
|
36
|
+
durationMs = options.durationMs ?? 60000;
|
|
37
|
+
const signal = options.signal;
|
|
38
|
+
if (typeof name !== "string" || !name || name.length > 256)
|
|
39
|
+
fail("CLI_GOAL_INVALID_PLUGIN_NAME");
|
|
40
|
+
if (scope !== undefined && !["user", "project", "local"].includes(scope))
|
|
41
|
+
fail("CLI_GOAL_INVALID_PLUGIN_SCOPE");
|
|
42
|
+
if (
|
|
43
|
+
!Number.isSafeInteger(intervalMs) ||
|
|
44
|
+
intervalMs < 250 ||
|
|
45
|
+
intervalMs > 60000 ||
|
|
46
|
+
!Number.isSafeInteger(durationMs) ||
|
|
47
|
+
durationMs < 1000 ||
|
|
48
|
+
durationMs > 900000
|
|
49
|
+
)
|
|
50
|
+
fail("CLI_GOAL_INVALID_WATCH_LIMIT");
|
|
51
|
+
if (signal !== undefined && !(signal instanceof AbortSignal))
|
|
52
|
+
fail("CLI_GOAL_INVALID_WATCH_SIGNAL");
|
|
53
|
+
const host = cliGoalExtensionHost(TYPES.map((type) => `event:${type}`));
|
|
54
|
+
const admission = Object.freeze({ cwd, env, managedSettingsFile, host });
|
|
55
|
+
return (async function* () {
|
|
56
|
+
if (signal?.aborted) return;
|
|
57
|
+
const source = _deps.createProjectGoalConnection(connectionFile);
|
|
58
|
+
if (!source.events || typeof source.readEvents !== "function")
|
|
59
|
+
fail("CLI_GOAL_EVENT_CONNECTION_REQUIRED");
|
|
60
|
+
const goalRef = source.goalRef;
|
|
61
|
+
if (
|
|
62
|
+
id !== goalRef.id ||
|
|
63
|
+
(expectedStore !== undefined && expectedStore !== goalRef.storeId)
|
|
64
|
+
)
|
|
65
|
+
fail("CLI_GOAL_STORE_MISMATCH");
|
|
66
|
+
const matches = collectGoalExtensions(admission).extensions.filter(
|
|
67
|
+
(entry) =>
|
|
68
|
+
entry.name === name &&
|
|
69
|
+
(!scope || entry.scope === scope) &&
|
|
70
|
+
entry.declaration.events.some((type) => TYPES.includes(type)),
|
|
71
|
+
);
|
|
72
|
+
if (matches.length !== 1) fail("CLI_GOAL_EVENT_PLUGIN_UNAVAILABLE");
|
|
73
|
+
const plugin = matches[0];
|
|
74
|
+
const ref = Object.freeze({
|
|
75
|
+
name: plugin.name,
|
|
76
|
+
scope: plugin.scope,
|
|
77
|
+
version: plugin.version,
|
|
78
|
+
});
|
|
79
|
+
const declaration = digest(plugin.declaration);
|
|
80
|
+
function revalidatePlugin() {
|
|
81
|
+
const current = collectGoalExtensions(admission).extensions.find(
|
|
82
|
+
(entry) =>
|
|
83
|
+
entry.name === ref.name &&
|
|
84
|
+
entry.scope === ref.scope &&
|
|
85
|
+
entry.version === ref.version,
|
|
86
|
+
);
|
|
87
|
+
if (!current || digest(current.declaration) !== declaration)
|
|
88
|
+
fail("CLI_GOAL_EVENT_ADMISSION_CHANGED");
|
|
89
|
+
}
|
|
90
|
+
const deadline = performance.now() + durationMs;
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const stop = () => controller.abort();
|
|
93
|
+
const timer = setTimeout(stop, durationMs);
|
|
94
|
+
timer.unref();
|
|
95
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
96
|
+
if (signal?.aborted) stop();
|
|
97
|
+
async function read() {
|
|
98
|
+
revalidatePlugin();
|
|
99
|
+
const value = await source.readEvents(id, { signal: controller.signal });
|
|
100
|
+
revalidatePlugin();
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
async function revalidate(snapshot) {
|
|
104
|
+
if (digest(await read()) !== digest(snapshot))
|
|
105
|
+
fail("CLI_GOAL_EVENT_SOURCE_CHANGED");
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
let previous = await read();
|
|
109
|
+
await revalidate(previous);
|
|
110
|
+
if (controller.signal.aborted) return;
|
|
111
|
+
yield Object.freeze({
|
|
112
|
+
schema: SCHEMA,
|
|
113
|
+
kind: "ready",
|
|
114
|
+
source: "desktop-project",
|
|
115
|
+
plugin: ref,
|
|
116
|
+
goalRef,
|
|
117
|
+
observedAt: new Date().toISOString(),
|
|
118
|
+
delivery: "polling",
|
|
119
|
+
replay: false,
|
|
120
|
+
});
|
|
121
|
+
while (!controller.signal.aborted && performance.now() < deadline) {
|
|
122
|
+
await delay(
|
|
123
|
+
Math.min(intervalMs, Math.max(1, deadline - performance.now())),
|
|
124
|
+
undefined,
|
|
125
|
+
{ signal: controller.signal },
|
|
126
|
+
);
|
|
127
|
+
if (controller.signal.aborted || performance.now() >= deadline) return;
|
|
128
|
+
const current = await read();
|
|
129
|
+
if (digest(current) === digest(previous)) continue;
|
|
130
|
+
const events = [];
|
|
131
|
+
if (
|
|
132
|
+
(previous.risk.reviewId && !current.risk.reviewId) ||
|
|
133
|
+
(previous.receipts.state === "recorded" &&
|
|
134
|
+
current.receipts.state !== "recorded")
|
|
135
|
+
)
|
|
136
|
+
fail("CLI_GOAL_EVENT_SOURCE_REGRESSED");
|
|
137
|
+
if (
|
|
138
|
+
current.risk.reviewId &&
|
|
139
|
+
digest(current.risk) !== digest(previous.risk) &&
|
|
140
|
+
plugin.declaration.events.includes(TYPES[0])
|
|
141
|
+
)
|
|
142
|
+
events.push({
|
|
143
|
+
type: TYPES[0],
|
|
144
|
+
summary: `Historical risk check ${current.risk.reviewId}: ${current.risk.state}; risk task count ${current.risk.riskTaskCount ?? "unknown"}. Current risk and goal completion require native verification.`,
|
|
145
|
+
});
|
|
146
|
+
for (const receipt of current.receipts.entries) {
|
|
147
|
+
const before = previous.receipts.entries.find(
|
|
148
|
+
(entry) => entry.id === receipt.id,
|
|
149
|
+
);
|
|
150
|
+
if (
|
|
151
|
+
before &&
|
|
152
|
+
before.status !== "unresolved" &&
|
|
153
|
+
digest(before) !== digest(receipt)
|
|
154
|
+
)
|
|
155
|
+
fail("CLI_GOAL_EVENT_SOURCE_REGRESSED");
|
|
156
|
+
if (
|
|
157
|
+
(!before || digest(receipt) !== digest(before)) &&
|
|
158
|
+
plugin.declaration.events.includes(TYPES[1])
|
|
159
|
+
)
|
|
160
|
+
events.push({
|
|
161
|
+
type: TYPES[1],
|
|
162
|
+
summary: `Historical action receipt ${receipt.id}: ${receipt.status} (${receipt.actionType}). This does not prove goal completion.`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const candidate of events) {
|
|
166
|
+
const observedAt = new Date().toISOString();
|
|
167
|
+
const event = await _deps.projectGoalExtensionEvent(
|
|
168
|
+
ref,
|
|
169
|
+
{ type: candidate.type, goalRef },
|
|
170
|
+
{
|
|
171
|
+
...admission,
|
|
172
|
+
async authorize(context) {
|
|
173
|
+
if (
|
|
174
|
+
context.operation !== "read-event" ||
|
|
175
|
+
context.type !== candidate.type ||
|
|
176
|
+
JSON.stringify(context.goalRef) !== JSON.stringify(goalRef)
|
|
177
|
+
)
|
|
178
|
+
return false;
|
|
179
|
+
await revalidate(current);
|
|
180
|
+
return true;
|
|
181
|
+
},
|
|
182
|
+
async readEvent() {
|
|
183
|
+
await revalidate(current);
|
|
184
|
+
return { summary: candidate.summary, occurredAt: observedAt };
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
await revalidate(current);
|
|
189
|
+
if (controller.signal.aborted || performance.now() >= deadline)
|
|
190
|
+
return;
|
|
191
|
+
yield Object.freeze({
|
|
192
|
+
schema: SCHEMA,
|
|
193
|
+
kind: "event",
|
|
194
|
+
source: "desktop-project",
|
|
195
|
+
plugin: ref,
|
|
196
|
+
observedAt,
|
|
197
|
+
delivery: "polling",
|
|
198
|
+
replay: false,
|
|
199
|
+
event,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
previous = current;
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!controller.signal.aborted) throw error;
|
|
206
|
+
} finally {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
signal?.removeEventListener("abort", stop);
|
|
209
|
+
controller.abort();
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "goal-project-observer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Observe bounded native risk-check and action-receipt summaries through a separately authorized desktop event connection",
|
|
5
|
+
"permissions": {
|
|
6
|
+
"goalExtension": [
|
|
7
|
+
"event:risk.review.completed",
|
|
8
|
+
"event:action.receipt.updated"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"goalExtension": {
|
|
12
|
+
"apiVersion": "1.0.0",
|
|
13
|
+
"hosts": {
|
|
14
|
+
"cli": ">=0.166.142"
|
|
15
|
+
},
|
|
16
|
+
"events": ["risk.review.completed", "action.receipt.updated"],
|
|
17
|
+
"panels": [],
|
|
18
|
+
"proposals": []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "goal-status-observer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Observe status changes of one explicitly selected standalone CLI goal",
|
|
5
|
+
"permissions": {
|
|
6
|
+
"goalExtension": ["event:goal.status.changed"]
|
|
7
|
+
},
|
|
8
|
+
"goalExtension": {
|
|
9
|
+
"apiVersion": "1.0.0",
|
|
10
|
+
"hosts": {
|
|
11
|
+
"cli": ">=0.166.142"
|
|
12
|
+
},
|
|
13
|
+
"events": ["goal.status.changed"],
|
|
14
|
+
"panels": [],
|
|
15
|
+
"proposals": []
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Project delivery assistant 0.2.0
|
|
2
|
+
|
|
3
|
+
This data-only scenario package combines evidence panels, bounded observations,
|
|
4
|
+
retained event replay and user-selected task-description drafts. It runs through
|
|
5
|
+
the existing CLI and both IDE plugin managers. Requires CLI 0.166.142, Session
|
|
6
|
+
Core 0.3.25 and a matching desktop build with the separately authorized goal
|
|
7
|
+
connections. Paired IDE releases are Open VSX 0.37.178 and JetBrains 0.4.195.
|
|
8
|
+
The version ranges in the manifest are compatibility floors; each operation
|
|
9
|
+
still needs its native source authorization.
|
|
10
|
+
|
|
11
|
+
The package contains no hooks, binaries, arbitrary code or autonomous scheduler.
|
|
12
|
+
It does not create tasks, choose a description for you, approve actions or mark a
|
|
13
|
+
goal complete. Task creation remains a future extension surface.
|
|
14
|
+
|
|
15
|
+
From the repository root, install at the scope you will use:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
cc plugin add packages/cli/src/lib/plugin-runtime/examples/project-delivery-assistant --scope project --json
|
|
19
|
+
cc plugin trust project-delivery-assistant --scope project
|
|
20
|
+
cc plugin consent project-delivery-assistant --scope project --grant --json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Consent covers the full declared capability set. Each command still needs its
|
|
24
|
+
own selected source and separate desktop authorization:
|
|
25
|
+
|
|
26
|
+
| Entry | Source and result |
|
|
27
|
+
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `goal panels` | A separately authorized evidence connection; objective/status and historical risk/receipt summary. A summary-only connection cannot supply evidence fields. |
|
|
29
|
+
| `goal watch` with a connection | A v4 event connection; risk-check and action-receipt observations for a bounded interval. Polling can miss intermediate changes. |
|
|
30
|
+
| `goal watch` without a connection | A standalone CLI goal; status changes only. This does not subscribe to native project status. |
|
|
31
|
+
| `goal replay` | A v5 retained-history connection and explicit new/resume checkpoint; one page per invocation. |
|
|
32
|
+
| `goal draft-access`, `draft-sources`, `draft-send`, `draft-receipt` | A separate v3 pending-draft authorization bound to this plugin/version/declaration. The user selects a current native source and a UTF-8 description file. |
|
|
33
|
+
|
|
34
|
+
In the desktop goal area, export the relevant connection with its separate native
|
|
35
|
+
confirmation. Connections are short-lived and tied to the active identity and
|
|
36
|
+
goal revision; keep their private files local. One connection does not grant the
|
|
37
|
+
other surfaces. For example:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
cc goal panels <goal-id> --connection "<evidence.json>" --plugin project-delivery-assistant --scope project --json
|
|
41
|
+
cc goal watch <goal-id> --connection "<events-v4.json>" --plugin project-delivery-assistant --scope project --duration 60 --json
|
|
42
|
+
cc goal replay <goal-id> --connection "<history-v5.json>" --checkpoint "<checkpoint.json>" --checkpoint-mode new --plugin project-delivery-assistant --scope project --json
|
|
43
|
+
# Subsequent explicit page: use the same checkpoint and --checkpoint-mode resume.
|
|
44
|
+
cc goal replay <goal-id> --connection "<history-v5.json>" --checkpoint "<checkpoint.json>" --checkpoint-mode resume --plugin project-delivery-assistant --scope project --json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Replay scans at most 20 retained events per page. Exit zero confirms checkpoint
|
|
48
|
+
commit; output alone does not. Keep the checkpoint after interruption because it
|
|
49
|
+
may already have advanced. Delivery can repeat event IDs. The newest 1000
|
|
50
|
+
instrumented events are retained; pre-instrumentation history is not backfilled.
|
|
51
|
+
Retention gaps and database regression are reported, without automatic reset.
|
|
52
|
+
|
|
53
|
+
For a pending description draft, export this plugin's access request first:
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
cc goal draft-access --plugin project-delivery-assistant --scope project --out "<new-access.json>" --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
In the desktop, choose the separate online-draft authorization and select that
|
|
60
|
+
access file. In either IDE select **Send online draft…** / **Submit pending
|
|
61
|
+
draft…**, pick the v3 connection, a current native source, the proposed description
|
|
62
|
+
and a new submission-record file. Alternatively use the CLI:
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
cc goal draft-sources <goal-id> --connection "<draft-v3.json>" --plugin project-delivery-assistant --scope project --json
|
|
66
|
+
cc goal draft-send <goal-id> --connection "<draft-v3.json>" --plugin project-delivery-assistant --scope project --source "<source-id>" --request-id "<stable-request-id>" --title "Delivery follow-up" --description-file "<description.txt>" --record "<new-submission.json>" --json
|
|
67
|
+
cc goal draft-receipt <goal-id> --connection "<draft-v3.json>" --plugin project-delivery-assistant --scope project --record "<submission.json>" --json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A saved draft is pending. Open it in the desktop, inspect the current native
|
|
71
|
+
preview, then confirm through the existing action flow. Read the real receipt
|
|
72
|
+
and run a fresh risk check to assess the outcome; a description edit does not
|
|
73
|
+
prove that a deadline or dependency risk was resolved. If submission is unknown,
|
|
74
|
+
keep its record and look up the receipt before deciding whether to send again.
|
|
75
|
+
|
|
76
|
+
In VS Code / JetBrains choose this installed plugin, then **CLI goal panels…**,
|
|
77
|
+
**Observe desktop events…**, **Recover retained history…** or the draft controls.
|
|
78
|
+
Recovery uses native connection and new/resume checkpoint dialogs; **Read next
|
|
79
|
+
page / check again** is explicit. No automatic observation, retry or business
|
|
80
|
+
action starts when the package is installed or reinstalled.
|
|
81
|
+
|
|
82
|
+
Disable or uninstall using the normal plugin controls. New operations are refused,
|
|
83
|
+
and active observers stop when they recheck admission. Native goals, actions,
|
|
84
|
+
receipts and saved checkpoints stay in their existing stores. Reinstalling keeps
|
|
85
|
+
the existing trust/consent policy, and requires a new explicit command or IDE
|
|
86
|
+
action to read again; it does not reconnect by itself. Version changes require
|
|
87
|
+
project trust again, capability expansion requires new consent, and a changed
|
|
88
|
+
declaration/goal binding requires a matching new desktop authorization and an
|
|
89
|
+
explicit new checkpoint when the old binding no longer matches.
|
|
90
|
+
|
|
91
|
+
Real IDE/Electron GUI journeys, automatic drafting, task-creation proposals,
|
|
92
|
+
independent business-effect evaluation and release gates remain pending.
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-delivery-assistant",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Project evidence, bounded event observation and retained replay, and user-selected description drafts through separately authorized CLI surfaces",
|
|
5
5
|
"permissions": {
|
|
6
6
|
"goalExtension": [
|
|
7
7
|
"event:goal.status.changed",
|
|
8
8
|
"event:risk.review.completed",
|
|
9
9
|
"event:action.receipt.updated",
|
|
10
10
|
"panel:goal.evidence",
|
|
11
|
-
"proposal:task.create",
|
|
12
11
|
"proposal:task.description.update"
|
|
13
12
|
]
|
|
14
13
|
},
|
|
15
14
|
"goalExtension": {
|
|
16
15
|
"apiVersion": "1.0.0",
|
|
17
16
|
"hosts": {
|
|
18
|
-
"cli": ">=0.166.
|
|
19
|
-
"vscode": ">=0.37.
|
|
20
|
-
"jetbrains": ">=0.4.
|
|
17
|
+
"cli": ">=0.166.142",
|
|
18
|
+
"vscode": ">=0.37.178",
|
|
19
|
+
"jetbrains": ">=0.4.195"
|
|
21
20
|
},
|
|
22
21
|
"events": [
|
|
23
22
|
"goal.status.changed",
|
|
@@ -33,6 +32,6 @@
|
|
|
33
32
|
"fallback": "text"
|
|
34
33
|
}
|
|
35
34
|
],
|
|
36
|
-
"proposals": ["task.
|
|
35
|
+
"proposals": ["task.description.update"]
|
|
37
36
|
}
|
|
38
37
|
}
|
|
@@ -36,6 +36,11 @@ function fixedHostOptions(options) {
|
|
|
36
36
|
id: options.host?.id,
|
|
37
37
|
version: options.host?.version,
|
|
38
38
|
capabilities: Object.freeze([...(options.host?.capabilities || [])]),
|
|
39
|
+
activeCapabilities: Object.freeze([
|
|
40
|
+
...(options.host?.activeCapabilities ??
|
|
41
|
+
options.host?.capabilities ??
|
|
42
|
+
[]),
|
|
43
|
+
]),
|
|
39
44
|
}),
|
|
40
45
|
authorize: options.authorize,
|
|
41
46
|
readEvent: options.readEvent,
|
|
@@ -44,6 +49,14 @@ function fixedHostOptions(options) {
|
|
|
44
49
|
});
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function requireActiveCapability(options, capability) {
|
|
53
|
+
if (
|
|
54
|
+
!options.host.capabilities.includes(capability) ||
|
|
55
|
+
!options.host.activeCapabilities.includes(capability)
|
|
56
|
+
)
|
|
57
|
+
throw new Error("goal extension capability is inactive for this operation");
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
/** Compatibility and mandatory consent are independent of legacy policy flags. */
|
|
48
61
|
export function collectGoalExtensions(options = {}) {
|
|
49
62
|
const extensions = [];
|
|
@@ -139,6 +152,7 @@ export async function projectGoalExtensionEvent(ref, request, options = {}) {
|
|
|
139
152
|
options = fixedHostOptions(options);
|
|
140
153
|
const type = request?.type;
|
|
141
154
|
const plugin = admittedPlugin(ref, options, "events", type);
|
|
155
|
+
requireActiveCapability(options, `event:${type}`);
|
|
142
156
|
const source = goalRef(request.goalRef);
|
|
143
157
|
const context = Object.freeze({
|
|
144
158
|
plugin: Object.freeze({
|
|
@@ -182,6 +196,7 @@ export async function projectGoalExtensionPanel(ref, request, options = {}) {
|
|
|
182
196
|
(entry) => entry.id === request?.panelId,
|
|
183
197
|
);
|
|
184
198
|
if (!panel) throw new Error("goal extension panel is undeclared");
|
|
199
|
+
requireActiveCapability(options, `panel:${panel.slot}`);
|
|
185
200
|
const source = goalRef(request.goalRef);
|
|
186
201
|
const context = Object.freeze({
|
|
187
202
|
plugin: Object.freeze({
|
|
@@ -229,6 +244,7 @@ export async function proposeGoalExtensionAction(ref, raw, options = {}) {
|
|
|
229
244
|
raw = normalizeGoalExtensionProposal(raw);
|
|
230
245
|
if (!plugin.declaration.proposals.includes(raw.type))
|
|
231
246
|
throw new Error("goal extension proposal is undeclared");
|
|
247
|
+
requireActiveCapability(options, `proposal:${raw.type}`);
|
|
232
248
|
const source = raw.goalRef;
|
|
233
249
|
const title = raw.title;
|
|
234
250
|
const description = raw.description;
|
|
@@ -5,7 +5,11 @@ import http from "node:http";
|
|
|
5
5
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { inspectPrivatePath } from "@chainlesschain/session-core/private-storage";
|
|
7
7
|
import { goalExtensionRef } from "@chainlesschain/session-core/goal-extension-proposal-contract";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
validateGoalEvidenceProjection,
|
|
10
|
+
validateGoalEventHistoryPage,
|
|
11
|
+
parseGoalEventCursor,
|
|
12
|
+
} from "@chainlesschain/session-core/goal-evidence-contract";
|
|
9
13
|
import { validateGoalDraftDescriptor } from "@chainlesschain/session-core/goal-draft-connection-contract";
|
|
10
14
|
import { createProjectGoalDraftClient } from "./project-goal-draft-connection.js";
|
|
11
15
|
function fail() {
|
|
@@ -59,17 +63,32 @@ export function createProjectGoalConnection(file) {
|
|
|
59
63
|
}
|
|
60
64
|
const evidence =
|
|
61
65
|
descriptor.schema === "chainlesschain.project-goal-connection/v2";
|
|
62
|
-
const
|
|
66
|
+
const events =
|
|
67
|
+
descriptor.schema === "chainlesschain.project-goal-connection/v4";
|
|
68
|
+
const history =
|
|
69
|
+
descriptor.schema === "chainlesschain.project-goal-connection/v5";
|
|
70
|
+
const endpointPath = history
|
|
71
|
+
? "/goal-event-history"
|
|
72
|
+
: events
|
|
73
|
+
? "/goal-events"
|
|
74
|
+
: evidence
|
|
75
|
+
? "/goal-evidence"
|
|
76
|
+
: "/goal-summary";
|
|
63
77
|
if (
|
|
64
78
|
Object.keys(descriptor).sort().join() !==
|
|
65
|
-
(evidence
|
|
79
|
+
(evidence || events || history
|
|
66
80
|
? "capability,endpoint,expiresAt,goalRef,key,schema"
|
|
67
81
|
: "endpoint,expiresAt,goalRef,key,schema") ||
|
|
68
|
-
(
|
|
69
|
-
? descriptor.capability !== "goal.
|
|
70
|
-
:
|
|
82
|
+
(history
|
|
83
|
+
? descriptor.capability !== "goal.event-history"
|
|
84
|
+
: events
|
|
85
|
+
? descriptor.capability !== "goal.events"
|
|
86
|
+
: evidence
|
|
87
|
+
? descriptor.capability !== "goal.evidence"
|
|
88
|
+
: descriptor.schema !==
|
|
89
|
+
"chainlesschain.project-goal-connection/v1") ||
|
|
71
90
|
typeof descriptor.endpoint !== "string" ||
|
|
72
|
-
!/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}\/goal-(?:summary|evidence)$/.test(
|
|
91
|
+
!/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}\/goal-(?:summary|evidence|events|event-history)$/.test(
|
|
73
92
|
descriptor.endpoint,
|
|
74
93
|
) ||
|
|
75
94
|
new URL(descriptor.endpoint).pathname !== endpointPath ||
|
|
@@ -93,22 +112,26 @@ export function createProjectGoalConnection(file) {
|
|
|
93
112
|
createHmac("sha256", Buffer.from(descriptor.key, "hex"))
|
|
94
113
|
.update(value)
|
|
95
114
|
.digest("hex");
|
|
96
|
-
async function read(id = ref.id) {
|
|
115
|
+
async function read(id = ref.id, { signal, after } = {}) {
|
|
97
116
|
validate();
|
|
98
117
|
if (id !== ref.id) fail();
|
|
118
|
+
if (after !== undefined && !history) fail();
|
|
119
|
+
if (after !== undefined) parseGoalEventCursor(after);
|
|
120
|
+
const requestPath =
|
|
121
|
+
endpointPath + (after === undefined ? "" : `?after=${after}`);
|
|
99
122
|
const nonce = randomBytes(32).toString("hex");
|
|
100
123
|
try {
|
|
101
124
|
const result = await new Promise((resolve, reject) => {
|
|
102
125
|
const request = http.get(
|
|
103
|
-
descriptor.endpoint
|
|
126
|
+
descriptor.endpoint +
|
|
127
|
+
(after === undefined ? "" : `?after=${after}`),
|
|
104
128
|
{
|
|
105
129
|
agent: false,
|
|
130
|
+
signal,
|
|
106
131
|
maxHeaderSize: 4096,
|
|
107
132
|
headers: {
|
|
108
133
|
"X-CC-Nonce": nonce,
|
|
109
|
-
"X-CC-Signature": mac(
|
|
110
|
-
`request\nGET\n${endpointPath}\n${nonce}`,
|
|
111
|
-
),
|
|
134
|
+
"X-CC-Signature": mac(`request\nGET\n${requestPath}\n${nonce}`),
|
|
112
135
|
},
|
|
113
136
|
},
|
|
114
137
|
(response) => {
|
|
@@ -124,19 +147,48 @@ export function createProjectGoalConnection(file) {
|
|
|
124
147
|
const body = Buffer.concat(chunks).toString("utf8"),
|
|
125
148
|
signature = response.headers["x-cc-signature"];
|
|
126
149
|
if (
|
|
127
|
-
response.statusCode !== 200 ||
|
|
150
|
+
(!history && response.statusCode !== 200) ||
|
|
128
151
|
typeof signature !== "string" ||
|
|
129
152
|
!/^[a-f0-9]{64}$/.test(signature) ||
|
|
130
153
|
!timingSafeEqual(
|
|
131
154
|
Buffer.from(signature),
|
|
132
|
-
Buffer.from(
|
|
155
|
+
Buffer.from(
|
|
156
|
+
mac(
|
|
157
|
+
history
|
|
158
|
+
? `response\n${nonce}\n${response.statusCode}\n${body}`
|
|
159
|
+
: `response\n${nonce}\n${body}`,
|
|
160
|
+
),
|
|
161
|
+
),
|
|
133
162
|
)
|
|
134
163
|
) {
|
|
135
164
|
reject(new Error("signature"));
|
|
136
165
|
return;
|
|
137
166
|
}
|
|
138
167
|
try {
|
|
139
|
-
|
|
168
|
+
const parsed = JSON.parse(body);
|
|
169
|
+
if (history && response.statusCode !== 200) {
|
|
170
|
+
if (
|
|
171
|
+
[409, 410].includes(response.statusCode) &&
|
|
172
|
+
Object.keys(parsed).join() === "error" &&
|
|
173
|
+
[
|
|
174
|
+
"GOAL_EVENT_CURSOR_INVALID",
|
|
175
|
+
"GOAL_EVENT_HISTORY_GAP",
|
|
176
|
+
"GOAL_EVENT_HISTORY_REGRESSED",
|
|
177
|
+
"GOAL_EVENT_HISTORY_UNAVAILABLE",
|
|
178
|
+
"GOAL_EVENT_HISTORY_INVALID",
|
|
179
|
+
].includes(parsed.error)
|
|
180
|
+
) {
|
|
181
|
+
reject(
|
|
182
|
+
Object.assign(new Error(parsed.error), {
|
|
183
|
+
code: parsed.error,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
reject(new Error("status"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
resolve(parsed);
|
|
140
192
|
} catch {
|
|
141
193
|
reject(new Error("json"));
|
|
142
194
|
}
|
|
@@ -152,34 +204,66 @@ export function createProjectGoalConnection(file) {
|
|
|
152
204
|
request.on("error", reject);
|
|
153
205
|
});
|
|
154
206
|
validate();
|
|
207
|
+
if (history) return validateGoalEventHistoryPage(result, ref, after);
|
|
155
208
|
if (
|
|
156
209
|
Object.keys(result).sort().join() !==
|
|
157
|
-
(
|
|
158
|
-
? "goalRef,
|
|
159
|
-
:
|
|
210
|
+
(events
|
|
211
|
+
? "goalRef,receipts,risk,schema"
|
|
212
|
+
: evidence
|
|
213
|
+
? "goalRef,objective,receipts,risk,schema,status"
|
|
214
|
+
: "goalRef,objective,schema,status") ||
|
|
160
215
|
result.schema !==
|
|
161
|
-
(
|
|
162
|
-
? "chainlesschain.project-goal-
|
|
163
|
-
:
|
|
216
|
+
(events
|
|
217
|
+
? "chainlesschain.project-goal-events/v1"
|
|
218
|
+
: evidence
|
|
219
|
+
? "chainlesschain.project-goal-evidence/v1"
|
|
220
|
+
: "chainlesschain.project-goal-summary/v1") ||
|
|
164
221
|
JSON.stringify(goalExtensionRef(result.goalRef)) !==
|
|
165
222
|
JSON.stringify(ref) ||
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
223
|
+
(!events &&
|
|
224
|
+
(typeof result.objective !== "string" ||
|
|
225
|
+
result.objective.length > 4096 ||
|
|
226
|
+
result.status !== "active"))
|
|
169
227
|
)
|
|
170
228
|
fail();
|
|
171
|
-
if (evidence)
|
|
229
|
+
if (evidence || events)
|
|
172
230
|
validateGoalEvidenceProjection({
|
|
173
231
|
risk: result.risk,
|
|
174
232
|
receipts: result.receipts,
|
|
175
233
|
});
|
|
176
234
|
return { ...result, goalRef: ref };
|
|
177
|
-
} catch {
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (
|
|
237
|
+
history &&
|
|
238
|
+
[
|
|
239
|
+
"GOAL_EVENT_CURSOR_INVALID",
|
|
240
|
+
"GOAL_EVENT_HISTORY_GAP",
|
|
241
|
+
"GOAL_EVENT_HISTORY_REGRESSED",
|
|
242
|
+
"GOAL_EVENT_HISTORY_UNAVAILABLE",
|
|
243
|
+
"GOAL_EVENT_HISTORY_INVALID",
|
|
244
|
+
].includes(error.code)
|
|
245
|
+
)
|
|
246
|
+
throw error;
|
|
178
247
|
fail();
|
|
179
248
|
}
|
|
180
249
|
}
|
|
181
250
|
validate();
|
|
182
|
-
return
|
|
251
|
+
return history
|
|
252
|
+
? Object.freeze({
|
|
253
|
+
goalRef: ref,
|
|
254
|
+
eventHistory: true,
|
|
255
|
+
readEventPage: (after, options = {}) =>
|
|
256
|
+
read(ref.id, { ...options, after }),
|
|
257
|
+
read: async () => fail(),
|
|
258
|
+
})
|
|
259
|
+
: events
|
|
260
|
+
? Object.freeze({
|
|
261
|
+
goalRef: ref,
|
|
262
|
+
events: true,
|
|
263
|
+
readEvents: read,
|
|
264
|
+
read: async () => fail(),
|
|
265
|
+
})
|
|
266
|
+
: Object.freeze({ goalRef: ref, evidence, read });
|
|
183
267
|
} catch {
|
|
184
268
|
fail();
|
|
185
269
|
} finally {
|