@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.
@@ -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 scopes = [...this.pending.values()];
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 = await this.projection.snapshot(scope);
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 { lstatSync as lstatSync8, realpathSync as realpathSync3, statSync } from "node:fs";
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 must describe the physical cwd, not inherited alternate index/worktree overrides. */
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(([name]) => !name.startsWith("GIT_")),
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("git", ["rev-parse", "--show-toplevel"], {
187
- cwd: actual,
188
- env: gitEnvironment(),
189
- encoding: "utf8",
190
- timeout: 3000,
191
- maxBuffer: 8192,
192
- windowsHide: true,
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))