@abelxiaoxing/cadence 2.0.10 → 2.0.12
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 +97 -25
- package/docs/reuse-and-distribution.md +3 -3
- package/docs/service-management.md +81 -3
- package/package.json +1 -1
- package/src/native-build.ts +1 -1
- package/src/native-client.ts +15 -9
- package/src/operator-cli.mjs +13 -2
- package/src/sdk-worker.mjs +5 -1
- package/src/service/contracts/current.ts +8 -1
- package/src/service-cli.mjs +465 -75
- package/src/service-host.mjs +52 -13
- package/src/task-store.ts +44 -10
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +7 -7
package/src/service-host.mjs
CHANGED
|
@@ -218,15 +218,19 @@ function readBounded(path, max, packaged = false) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
function gitEnvironment() {
|
|
221
|
+
const root = process.getuid?.() === 0;
|
|
221
222
|
return {
|
|
222
|
-
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_"))),
|
|
223
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_") && !(root && name === "SUDO_UID"))),
|
|
223
224
|
GIT_OPTIONAL_LOCKS: "0",
|
|
224
225
|
LC_ALL: "C"
|
|
225
226
|
};
|
|
226
227
|
}
|
|
228
|
+
function gitArguments(args) {
|
|
229
|
+
return process.platform !== "win32" && process.getuid?.() === 0 ? ["-c", "safe.directory=", "-c", "core.fsmonitor=false", ...args] : args;
|
|
230
|
+
}
|
|
227
231
|
function resolveWorkspace(cwd) {
|
|
228
232
|
const actual = realpathSync(cwd);
|
|
229
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
233
|
+
const result = spawnSync("git", gitArguments(["rev-parse", "--show-toplevel"]), {
|
|
230
234
|
cwd: actual,
|
|
231
235
|
env: gitEnvironment(),
|
|
232
236
|
encoding: "utf8",
|
|
@@ -288,12 +292,17 @@ function contains(root, path) {
|
|
|
288
292
|
const within = relative(root, resolve(path));
|
|
289
293
|
return !within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within);
|
|
290
294
|
}
|
|
295
|
+
function privateWorkspace(requested, stateHome = defaultStateHome()) {
|
|
296
|
+
return contains(join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"), canonicalAmbientRoot(requested));
|
|
297
|
+
}
|
|
291
298
|
class TaskStore {
|
|
292
299
|
root;
|
|
293
300
|
tasks;
|
|
294
301
|
privateRoot;
|
|
295
302
|
constructor(root, stateHome = defaultStateHome()) {
|
|
296
303
|
this.root = realpathSync(root);
|
|
304
|
+
if (privateWorkspace(this.root, stateHome))
|
|
305
|
+
throw new Error("Private state cannot be a workspace");
|
|
297
306
|
this.tasks = join(this.root, ".cadence", "assistant", "tasks");
|
|
298
307
|
this.privateRoot = privateRootFor(this.root, stateHome);
|
|
299
308
|
if (contains(this.root, this.privateRoot))
|
|
@@ -3861,7 +3870,8 @@ function savedGraph(v) {
|
|
|
3861
3870
|
return { status: "unavailable", error: noEffect2(o.error) };
|
|
3862
3871
|
}
|
|
3863
3872
|
function correlate(v) {
|
|
3864
|
-
|
|
3873
|
+
const taskMatches = v.task ? v.task.id === v.request.taskId : v.request.taskId === null || v.context.status === "unavailable";
|
|
3874
|
+
if (!taskMatches || controlWorkspace(v.context) !== v.workspaceId || controlTask(v.context) !== v.request.taskId)
|
|
3865
3875
|
invalid();
|
|
3866
3876
|
if (v.context.status === "available") {
|
|
3867
3877
|
const c = v.context.version;
|
|
@@ -5233,6 +5243,22 @@ function emptyScan() {
|
|
|
5233
5243
|
}
|
|
5234
5244
|
|
|
5235
5245
|
// src/history/live.ts
|
|
5246
|
+
function scopedSnapshot(source, scope) {
|
|
5247
|
+
if (scope.kind === "task" || source.scope.kind === "child")
|
|
5248
|
+
return;
|
|
5249
|
+
if (!source.runs.some((run) => scope.kind === "run" ? run.runId === scope.id && run.childRunId === null : run.childRunId === scope.id))
|
|
5250
|
+
return;
|
|
5251
|
+
return decodeSnapshot({
|
|
5252
|
+
...source,
|
|
5253
|
+
scope,
|
|
5254
|
+
runs: source.runs.filter((run) => scope.kind === "run" ? run.runId === scope.id : run.childRunId === scope.id),
|
|
5255
|
+
horizon: {
|
|
5256
|
+
...source.horizon,
|
|
5257
|
+
historicalRuns: { ...source.horizon.historicalRuns, scope }
|
|
5258
|
+
}
|
|
5259
|
+
});
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5236
5262
|
class HistoryLive {
|
|
5237
5263
|
writer;
|
|
5238
5264
|
projection;
|
|
@@ -5346,8 +5372,10 @@ class HistoryLive {
|
|
|
5346
5372
|
}
|
|
5347
5373
|
}
|
|
5348
5374
|
async drain() {
|
|
5349
|
-
const
|
|
5375
|
+
const order = { task: 0, run: 1, child: 2 };
|
|
5376
|
+
const scopes = [...this.pending.values()].sort((a, b) => order[a.kind] - order[b.kind]);
|
|
5350
5377
|
this.pending.clear();
|
|
5378
|
+
const sources = [];
|
|
5351
5379
|
try {
|
|
5352
5380
|
for (const scope of scopes) {
|
|
5353
5381
|
if (this.closed)
|
|
@@ -5355,14 +5383,19 @@ class HistoryLive {
|
|
|
5355
5383
|
if (scope.kind !== "run" && ![...this.viewers].some((v) => canonicalJson(v.scope) === canonicalJson(scope)))
|
|
5356
5384
|
continue;
|
|
5357
5385
|
let snapshot;
|
|
5386
|
+
for (let i = sources.length - 1;i >= 0 && !snapshot; i--)
|
|
5387
|
+
snapshot = scopedSnapshot(sources[i], scope);
|
|
5388
|
+
const reused = snapshot !== undefined;
|
|
5358
5389
|
try {
|
|
5359
|
-
snapshot
|
|
5390
|
+
snapshot ??= await this.projection.snapshot(scope);
|
|
5360
5391
|
} catch {
|
|
5361
5392
|
for (const v of this.viewers)
|
|
5362
5393
|
if (v.scope.id === scope.id)
|
|
5363
5394
|
this.enqueue(v, this.watermark(v.scope));
|
|
5364
5395
|
continue;
|
|
5365
5396
|
}
|
|
5397
|
+
if (!reused && sources.length < LIMITS.viewers && (scope.kind === "task" || scopes.some((s) => s.kind === "child" && snapshot.runs.some((r) => r.childRunId === s.id))))
|
|
5398
|
+
sources.push(snapshot);
|
|
5366
5399
|
const key = `${scope.kind}/${scope.id}`, old = this.rings.get(key);
|
|
5367
5400
|
const update = (previousSeq, selected2 = scope, previousKeys) => decodeLiveEvent({
|
|
5368
5401
|
kind: "update",
|
|
@@ -14808,7 +14841,13 @@ function localOrigin(policy, actualPort) {
|
|
|
14808
14841
|
}
|
|
14809
14842
|
|
|
14810
14843
|
// src/service/server-config.ts
|
|
14811
|
-
import {
|
|
14844
|
+
import {
|
|
14845
|
+
existsSync as existsSync2,
|
|
14846
|
+
lstatSync as lstatSync8,
|
|
14847
|
+
mkdirSync as mkdirSync3,
|
|
14848
|
+
realpathSync as realpathSync3,
|
|
14849
|
+
statSync
|
|
14850
|
+
} from "node:fs";
|
|
14812
14851
|
import { dirname as dirname7, join as join10 } from "node:path";
|
|
14813
14852
|
|
|
14814
14853
|
// skills/_shared/load-config.mjs
|
|
@@ -18931,7 +18970,7 @@ function captureChanges(root) {
|
|
|
18931
18970
|
limits.push("Git observation time budget exhausted");
|
|
18932
18971
|
return { stdout: Buffer.alloc(0) };
|
|
18933
18972
|
}
|
|
18934
|
-
const result = spawnSync2("git", ["-c", "core.quotepath=true", ...args], {
|
|
18973
|
+
const result = spawnSync2("git", gitArguments(["-c", "core.quotepath=true", ...args]), {
|
|
18935
18974
|
cwd: root,
|
|
18936
18975
|
encoding: "buffer",
|
|
18937
18976
|
timeout: Math.max(1, deadline - Date.now()),
|
|
@@ -18979,7 +19018,7 @@ function captureChanges(root) {
|
|
|
18979
19018
|
limits.push(`Unreadable/changing: ${name}`);
|
|
18980
19019
|
}
|
|
18981
19020
|
};
|
|
18982
|
-
const probe = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
19021
|
+
const probe = spawnSync2("git", gitArguments(["rev-parse", "--is-inside-work-tree"]), {
|
|
18983
19022
|
cwd: root,
|
|
18984
19023
|
env: gitEnvironment(),
|
|
18985
19024
|
timeout: 3000,
|
|
@@ -22152,8 +22191,6 @@ class TaskActions {
|
|
|
22152
22191
|
const access = this.currentAccess();
|
|
22153
22192
|
const committedSeq = snapshot ?? h.writer.committedSeq;
|
|
22154
22193
|
const selected2 = taskId ? h.coordination.store.loadSnapshot(taskId) : null;
|
|
22155
|
-
if (taskId && !selected2)
|
|
22156
|
-
throw new ContractError("stale");
|
|
22157
22194
|
if (selected2 && realpathSync6(selected2.record.cwd) !== this.cwd)
|
|
22158
22195
|
throw new ContractError("unsupported-context");
|
|
22159
22196
|
const secrets = this.viewSecrets();
|
|
@@ -22166,7 +22203,7 @@ class TaskActions {
|
|
|
22166
22203
|
const plan = selected2 ? sanitizeText(selected2.record.plan, secrets) : null;
|
|
22167
22204
|
const editable = selected2 ? plan === selected2.record.plan : true;
|
|
22168
22205
|
let descriptor = null;
|
|
22169
|
-
let unavailable2 = null;
|
|
22206
|
+
let unavailable2 = taskId && !selected2 ? publicError("unavailable") : null;
|
|
22170
22207
|
let saved = { status: "none" };
|
|
22171
22208
|
if (selected2) {
|
|
22172
22209
|
try {
|
|
@@ -22290,6 +22327,8 @@ class TaskActions {
|
|
|
22290
22327
|
if (request.storeId !== this.options.host.reader.storeId)
|
|
22291
22328
|
throw new ContractError("stale");
|
|
22292
22329
|
const { view, changes, observation, secrets } = this.observeCurrent(request.taskId);
|
|
22330
|
+
if (!view.task)
|
|
22331
|
+
throw new ContractError("unavailable");
|
|
22293
22332
|
const text2 = sanitizeText(changes.text, secrets);
|
|
22294
22333
|
return decodeCurrentChanges({
|
|
22295
22334
|
request,
|
|
@@ -22360,7 +22399,7 @@ class TaskActions {
|
|
|
22360
22399
|
checkObligations(taskId) {
|
|
22361
22400
|
const { view, assertCurrent } = this.observeCurrent(taskId);
|
|
22362
22401
|
if (!view.task)
|
|
22363
|
-
throw new ContractError("
|
|
22402
|
+
throw new ContractError("unavailable");
|
|
22364
22403
|
const checks = this.obligationsFor(taskId, view.task.kind, view.context, view.committedSeq).checks;
|
|
22365
22404
|
assertCurrent();
|
|
22366
22405
|
return checks;
|
|
@@ -22885,7 +22924,7 @@ function refuseAt(stage, step) {
|
|
|
22885
22924
|
}
|
|
22886
22925
|
}
|
|
22887
22926
|
async function createServiceHost(value, sourceBuild) {
|
|
22888
|
-
const embedded = "
|
|
22927
|
+
const embedded = "e9b1ea4bb8d1c275a0cb0cf70947f9ac58e5de0cc438d551dc96809f3e82dd7c";
|
|
22889
22928
|
if (embedded && sourceBuild)
|
|
22890
22929
|
throw new ContractError("unsupported-context");
|
|
22891
22930
|
const contractHash = decodeHash(embedded ?? sourceBuild?.contractHash);
|
package/src/task-store.ts
CHANGED
|
@@ -171,26 +171,44 @@ export function readBounded(
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/** Git observations
|
|
174
|
+
/** Git observations describe the physical cwd, not inherited alternate
|
|
175
|
+
* index/worktree overrides. Root never borrows sudo's original user's trust. */
|
|
175
176
|
export function gitEnvironment(): NodeJS.ProcessEnv {
|
|
177
|
+
const root = process.getuid?.() === 0;
|
|
176
178
|
return {
|
|
177
179
|
...Object.fromEntries(
|
|
178
|
-
Object.entries(process.env).filter(
|
|
180
|
+
Object.entries(process.env).filter(
|
|
181
|
+
([name]) => !name.startsWith("GIT_") && !(root && name === "SUDO_UID"),
|
|
182
|
+
),
|
|
179
183
|
),
|
|
180
184
|
GIT_OPTIONAL_LOCKS: "0",
|
|
181
185
|
LC_ALL: "C",
|
|
182
186
|
};
|
|
183
187
|
}
|
|
188
|
+
/** Observation is not permission to trust another account's repository.
|
|
189
|
+
* Even with fsmonitor/textconv/ext-diff disabled, status/diff can invoke clean
|
|
190
|
+
* or process filters. Root therefore requires Git's own ownership check, with
|
|
191
|
+
* no safe.directory exceptions (including inherited wildcards). No config is
|
|
192
|
+
* written and ordinary-user trust behavior stays unchanged. */
|
|
193
|
+
export function gitArguments(args: string[]): string[] {
|
|
194
|
+
return process.platform !== "win32" && process.getuid?.() === 0
|
|
195
|
+
? ["-c", "safe.directory=", "-c", "core.fsmonitor=false", ...args]
|
|
196
|
+
: args;
|
|
197
|
+
}
|
|
184
198
|
export function resolveWorkspace(cwd: string): { root: string; cwd: string } {
|
|
185
199
|
const actual = realpathSync(cwd);
|
|
186
|
-
const result = spawnSync(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
const result = spawnSync(
|
|
201
|
+
"git",
|
|
202
|
+
gitArguments(["rev-parse", "--show-toplevel"]),
|
|
203
|
+
{
|
|
204
|
+
cwd: actual,
|
|
205
|
+
env: gitEnvironment(),
|
|
206
|
+
encoding: "utf8",
|
|
207
|
+
timeout: 3000,
|
|
208
|
+
maxBuffer: 8192,
|
|
209
|
+
windowsHide: true,
|
|
210
|
+
},
|
|
211
|
+
);
|
|
194
212
|
// Remove only Git's record terminator, not valid whitespace in a path.
|
|
195
213
|
const topLevel = result.stdout?.replace(
|
|
196
214
|
process.platform === "win32" ? /\r?\n$/ : /\n$/,
|
|
@@ -274,6 +292,18 @@ export function stateHomeInside(
|
|
|
274
292
|
const canonical = realpathSync(root);
|
|
275
293
|
return contains(canonical, privateRootFor(canonical, stateHome));
|
|
276
294
|
}
|
|
295
|
+
/** Also exclude projects *inside* our reserved private namespace. Root access
|
|
296
|
+
* does not turn private history/coordination into an ordinary engineering tree.
|
|
297
|
+
* Resolves missing tails without creating anything (for explicit project mkdir). */
|
|
298
|
+
export function privateWorkspace(
|
|
299
|
+
requested: string,
|
|
300
|
+
stateHome = defaultStateHome(),
|
|
301
|
+
): boolean {
|
|
302
|
+
return contains(
|
|
303
|
+
join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"),
|
|
304
|
+
canonicalAmbientRoot(requested),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
277
307
|
export const LANDING_WORKSPACE = "cadence";
|
|
278
308
|
/** A directory that contains the private state home (the home directory and
|
|
279
309
|
* its ancestors) can never be a workspace; use ~/cadence instead of refusing.
|
|
@@ -283,6 +313,8 @@ export function effectiveWorkspace(
|
|
|
283
313
|
stateHome?: string,
|
|
284
314
|
home = homedir(),
|
|
285
315
|
): { workspace: string; redirected: boolean } {
|
|
316
|
+
if (privateWorkspace(requested, stateHome))
|
|
317
|
+
throw new Error("Private state cannot be a workspace");
|
|
286
318
|
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
287
319
|
return { workspace: requested, redirected: false };
|
|
288
320
|
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
@@ -301,6 +333,8 @@ export class TaskStore {
|
|
|
301
333
|
readonly privateRoot: string;
|
|
302
334
|
constructor(root: string, stateHome = defaultStateHome()) {
|
|
303
335
|
this.root = realpathSync(root);
|
|
336
|
+
if (privateWorkspace(this.root, stateHome))
|
|
337
|
+
throw new Error("Private state cannot be a workspace");
|
|
304
338
|
this.tasks = join(this.root, ".cadence", "assistant", "tasks");
|
|
305
339
|
this.privateRoot = privateRootFor(this.root, stateHome);
|
|
306
340
|
if (contains(this.root, this.privateRoot))
|