local-operator-ui 0.25.7 → 0.25.8

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.
Files changed (2) hide show
  1. package/out/main/index.js +62 -36
  2. package/package.json +1 -1
package/out/main/index.js CHANGED
@@ -8125,9 +8125,6 @@ function receiptFor(entry, state, now) {
8125
8125
  expiresAt: now + ACCESS_RESULT_TTL_MS
8126
8126
  };
8127
8127
  }
8128
- function receiptForRequester(results, origin, requester, now) {
8129
- return Object.values(cleanResults(results, now)).filter((receipt) => receipt.origin === origin && receipt.requester === requester).sort((a, b) => b.decidedAt - a.decidedAt)[0];
8130
- }
8131
8128
  const APPROVALS_FILENAME = "approvals.json";
8132
8129
  function emptyStore() {
8133
8130
  return {
@@ -8244,17 +8241,13 @@ class ApprovalStore {
8244
8241
  * `request_access` → tells the user → `await_access`.
8245
8242
  */
8246
8243
  ensureTopLevelAccess(url, requester) {
8247
- if (this.originAllowed(url)) return { allowed: true, viaOnceGrant: false };
8248
- const grant = consumableGrant(
8249
- this.onceGrants,
8250
- url.origin,
8251
- requester,
8252
- this.now()
8253
- );
8254
- if (grant) {
8255
- delete this.onceGrants[url.origin];
8256
- this.persistIfChanged();
8257
- return { allowed: true, viaOnceGrant: true };
8244
+ const authority = this.liveAuthority(url, requester);
8245
+ if (authority) {
8246
+ if (authority.viaOnceGrant) {
8247
+ delete this.onceGrants[url.origin];
8248
+ this.persistIfChanged();
8249
+ }
8250
+ return { allowed: true, viaOnceGrant: authority.viaOnceGrant };
8258
8251
  }
8259
8252
  throw new BridgeCommandError(
8260
8253
  "origin_not_allowed",
@@ -8266,6 +8259,35 @@ class ApprovalStore {
8266
8259
  }
8267
8260
  );
8268
8261
  }
8262
+ /**
8263
+ * The live authority for one origin and requester, WITHOUT consuming it.
8264
+ *
8265
+ * THE SINGLE SOURCE OF TRUTH, and the fix for a shipped consent defect: the
8266
+ * gate and `request_access` used to answer from two different places —
8267
+ * `ensureTopLevelAccess` from the grant, `requestAccess` from a resolved
8268
+ * RECEIPT — and a receipt outlives the grant it was written for (its 15-minute
8269
+ * `ACCESS_RESULT_TTL_MS` against the 10-minute once-grant TTL), so
8270
+ * `request_access` answered `allowed` for a navigation the very next `open`
8271
+ * refused with `origin_not_allowed`, and raised no band while doing it: the
8272
+ * request was dropped without ever being put back to the user. Same pair
8273
+ * after a revocation, where the receipt outlives the grant by however long it
8274
+ * had left to run.
8275
+ *
8276
+ * `originAllowed` is the durable verdict plus the session grants; the second
8277
+ * term is an UNSPENT `once` grant bound to this requester, which the gate
8278
+ * consumes and this does not. Everything else is not authority: a spent grant,
8279
+ * a revoked one, a receipt for either.
8280
+ */
8281
+ liveAuthority(url, requester) {
8282
+ if (this.originAllowed(url)) return { viaOnceGrant: false };
8283
+ const grant = consumableGrant(
8284
+ this.onceGrants,
8285
+ url.origin,
8286
+ requester,
8287
+ this.now()
8288
+ );
8289
+ return grant ? { viaOnceGrant: true } : null;
8290
+ }
8269
8291
  /** Admission is captured once, but revocation remains authoritative while an
8270
8292
  * asynchronous navigation is in flight. A receipt is never transferable. */
8271
8293
  admit(url, requester) {
@@ -8318,29 +8340,24 @@ class ApprovalStore {
8318
8340
  * The rules are `vendor/driver/access-flow.ts`'s, applied to the queue: a repeat
8319
8341
  * request for the same origin by the same requester is idempotent with its
8320
8342
  * original TTL, a request for a different origin REPLACES the live one (with
8321
- * a tombstone for the displaced requester), and a live deny receipt answers
8322
- * "denied" without re-prompting until its cool-down expires.
8343
+ * a tombstone for the displaced requester), and a DURABLE denial answers
8344
+ * "denied" without re-prompting, because that is what the button promises —
8345
+ * "Don't allow" says the agent stops asking about this site until the denial is
8346
+ * revoked.
8323
8347
  */
8324
8348
  requestAccess(rawUrl, requester, kind = "async", commandId) {
8325
8349
  const url = safeHttpUrl(rawUrl);
8326
8350
  const now = this.now();
8327
- if (this.originAllowed(url)) {
8351
+ if (this.liveAuthority(url, requester)) {
8328
8352
  return { origin: url.origin, state: "allowed" };
8329
8353
  }
8354
+ if (this.refusalReason(url) === "denied") {
8355
+ return { origin: url.origin, state: "denied" };
8356
+ }
8330
8357
  const existing = findPending(this.queue, url.origin, requester, kind, now);
8331
8358
  if (existing) {
8332
8359
  return this.entryResponse(existing, "pending");
8333
8360
  }
8334
- const receipt = receiptForRequester(
8335
- this.results,
8336
- url.origin,
8337
- requester,
8338
- now
8339
- );
8340
- if (receipt && receipt.state !== "superseded") {
8341
- const state = receipt.state === "denied" ? "denied" : "allowed";
8342
- return { origin: url.origin, state, entry_id: receipt.entryId };
8343
- }
8344
8361
  this.sweep(now);
8345
8362
  if (liveQueue(this.queue, now).length >= ACCESS_QUEUE_CAP) {
8346
8363
  throw new BridgeCommandError(
@@ -8396,8 +8413,21 @@ class ApprovalStore {
8396
8413
  hostname: entry.displayAuthority,
8397
8414
  requester: entry.requester,
8398
8415
  requestedAt: entry.requestedAt,
8399
- expiresAt: entry.expiresAt,
8400
- ...this.decisionFor(entry) ? { decision: this.decisionFor(entry) } : {}
8416
+ expiresAt: entry.expiresAt
8417
+ /*
8418
+ * NO `decision` TERM, and the removal is the point. The vendored
8419
+ * `accessState()` takes an optional decision so that a LIVE record can read
8420
+ * `allowed`/`denied` before the grant lands, and this host fed it from the
8421
+ * decision receipts — but every arm that writes a receipt removes its entry
8422
+ * from the queue FIRST (`respond` filters, `cancelAccess` splices), entry ids
8423
+ * are minted per entry and nothing here is restored from disk, so a record
8424
+ * with a decision can never be the record this reads. It was unreachable code
8425
+ * carrying the residue of the defect this fix closes, and a reader would
8426
+ * reasonably infer from it that a receipt can still answer. It cannot:
8427
+ * `requestAccess` answers from `liveAuthority` and from a durable verdict, and
8428
+ * the receipts are the store's record of what was decided (swept by TTL)
8429
+ * rather than a reader's source.
8430
+ */
8401
8431
  } : void 0,
8402
8432
  this.tombstones,
8403
8433
  this.originAllowed(url),
@@ -8406,6 +8436,9 @@ class ApprovalStore {
8406
8436
  requester,
8407
8437
  now
8408
8438
  );
8439
+ if (state === "none" && this.refusalReason(url) === "denied") {
8440
+ return { origin: url.origin, state: "denied" };
8441
+ }
8409
8442
  return {
8410
8443
  origin: url.origin,
8411
8444
  state,
@@ -8668,13 +8701,6 @@ class ApprovalStore {
8668
8701
  this.onChanged();
8669
8702
  }
8670
8703
  // ---- internals -----------------------------------------------------------
8671
- decisionFor(entry) {
8672
- const receipt = this.results[resultKey(entry.entryId, entry.requester)];
8673
- if (!receipt) return void 0;
8674
- if (receipt.state === "denied") return "deny";
8675
- if (receipt.state === "allowed") return "site";
8676
- return void 0;
8677
- }
8678
8704
  /** Replace-don't-queue: the live entry for another origin (or another
8679
8705
  * requester) is displaced, and its requester gets a tombstone so its next poll
8680
8706
  * reads "superseded" rather than the neutral "none". */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-operator-ui",
3
- "version": "0.25.7",
3
+ "version": "0.25.8",
4
4
  "productName": "Local Operator",
5
5
  "description": "User interface for AI agent assistants that run Python code safely on your device through an intuitive chat interface. Supports local and cloud LLM models with built-in security features.",
6
6
  "main": "./out/main/index.js",