@abelxiaoxing/cadence 2.0.10 → 2.0.11
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 +83 -25
- package/docs/reuse-and-distribution.md +3 -3
- package/docs/service-management.md +81 -3
- package/package.json +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-cli.mjs +465 -75
- package/src/service-host.mjs +45 -7
- 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 +6 -6
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))
|
|
@@ -5233,6 +5242,22 @@ function emptyScan() {
|
|
|
5233
5242
|
}
|
|
5234
5243
|
|
|
5235
5244
|
// src/history/live.ts
|
|
5245
|
+
function scopedSnapshot(source, scope) {
|
|
5246
|
+
if (scope.kind === "task" || source.scope.kind === "child")
|
|
5247
|
+
return;
|
|
5248
|
+
if (!source.runs.some((run) => scope.kind === "run" ? run.runId === scope.id && run.childRunId === null : run.childRunId === scope.id))
|
|
5249
|
+
return;
|
|
5250
|
+
return decodeSnapshot({
|
|
5251
|
+
...source,
|
|
5252
|
+
scope,
|
|
5253
|
+
runs: source.runs.filter((run) => scope.kind === "run" ? run.runId === scope.id : run.childRunId === scope.id),
|
|
5254
|
+
horizon: {
|
|
5255
|
+
...source.horizon,
|
|
5256
|
+
historicalRuns: { ...source.horizon.historicalRuns, scope }
|
|
5257
|
+
}
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5236
5261
|
class HistoryLive {
|
|
5237
5262
|
writer;
|
|
5238
5263
|
projection;
|
|
@@ -5346,8 +5371,10 @@ class HistoryLive {
|
|
|
5346
5371
|
}
|
|
5347
5372
|
}
|
|
5348
5373
|
async drain() {
|
|
5349
|
-
const
|
|
5374
|
+
const order = { task: 0, run: 1, child: 2 };
|
|
5375
|
+
const scopes = [...this.pending.values()].sort((a, b) => order[a.kind] - order[b.kind]);
|
|
5350
5376
|
this.pending.clear();
|
|
5377
|
+
const sources = [];
|
|
5351
5378
|
try {
|
|
5352
5379
|
for (const scope of scopes) {
|
|
5353
5380
|
if (this.closed)
|
|
@@ -5355,14 +5382,19 @@ class HistoryLive {
|
|
|
5355
5382
|
if (scope.kind !== "run" && ![...this.viewers].some((v) => canonicalJson(v.scope) === canonicalJson(scope)))
|
|
5356
5383
|
continue;
|
|
5357
5384
|
let snapshot;
|
|
5385
|
+
for (let i = sources.length - 1;i >= 0 && !snapshot; i--)
|
|
5386
|
+
snapshot = scopedSnapshot(sources[i], scope);
|
|
5387
|
+
const reused = snapshot !== undefined;
|
|
5358
5388
|
try {
|
|
5359
|
-
snapshot
|
|
5389
|
+
snapshot ??= await this.projection.snapshot(scope);
|
|
5360
5390
|
} catch {
|
|
5361
5391
|
for (const v of this.viewers)
|
|
5362
5392
|
if (v.scope.id === scope.id)
|
|
5363
5393
|
this.enqueue(v, this.watermark(v.scope));
|
|
5364
5394
|
continue;
|
|
5365
5395
|
}
|
|
5396
|
+
if (!reused && sources.length < LIMITS.viewers && (scope.kind === "task" || scopes.some((s) => s.kind === "child" && snapshot.runs.some((r) => r.childRunId === s.id))))
|
|
5397
|
+
sources.push(snapshot);
|
|
5366
5398
|
const key = `${scope.kind}/${scope.id}`, old = this.rings.get(key);
|
|
5367
5399
|
const update = (previousSeq, selected2 = scope, previousKeys) => decodeLiveEvent({
|
|
5368
5400
|
kind: "update",
|
|
@@ -14808,7 +14840,13 @@ function localOrigin(policy, actualPort) {
|
|
|
14808
14840
|
}
|
|
14809
14841
|
|
|
14810
14842
|
// src/service/server-config.ts
|
|
14811
|
-
import {
|
|
14843
|
+
import {
|
|
14844
|
+
existsSync as existsSync2,
|
|
14845
|
+
lstatSync as lstatSync8,
|
|
14846
|
+
mkdirSync as mkdirSync3,
|
|
14847
|
+
realpathSync as realpathSync3,
|
|
14848
|
+
statSync
|
|
14849
|
+
} from "node:fs";
|
|
14812
14850
|
import { dirname as dirname7, join as join10 } from "node:path";
|
|
14813
14851
|
|
|
14814
14852
|
// skills/_shared/load-config.mjs
|
|
@@ -18931,7 +18969,7 @@ function captureChanges(root) {
|
|
|
18931
18969
|
limits.push("Git observation time budget exhausted");
|
|
18932
18970
|
return { stdout: Buffer.alloc(0) };
|
|
18933
18971
|
}
|
|
18934
|
-
const result = spawnSync2("git", ["-c", "core.quotepath=true", ...args], {
|
|
18972
|
+
const result = spawnSync2("git", gitArguments(["-c", "core.quotepath=true", ...args]), {
|
|
18935
18973
|
cwd: root,
|
|
18936
18974
|
encoding: "buffer",
|
|
18937
18975
|
timeout: Math.max(1, deadline - Date.now()),
|
|
@@ -18979,7 +19017,7 @@ function captureChanges(root) {
|
|
|
18979
19017
|
limits.push(`Unreadable/changing: ${name}`);
|
|
18980
19018
|
}
|
|
18981
19019
|
};
|
|
18982
|
-
const probe = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
19020
|
+
const probe = spawnSync2("git", gitArguments(["rev-parse", "--is-inside-work-tree"]), {
|
|
18983
19021
|
cwd: root,
|
|
18984
19022
|
env: gitEnvironment(),
|
|
18985
19023
|
timeout: 3000,
|
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))
|