@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.
@@ -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
- if (v.request.taskId !== (v.task?.id ?? null) || controlWorkspace(v.context) !== v.workspaceId || controlTask(v.context) !== v.request.taskId)
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 scopes = [...this.pending.values()];
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 = await this.projection.snapshot(scope);
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 { lstatSync as lstatSync8, realpathSync as realpathSync3, statSync } from "node:fs";
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("stale");
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 = "c2bb81519387005a71e88a64364dff47d5c39dab17812833ca898bc51055e59d";
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 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))