kifaru 1.0.177 → 1.0.178

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.
@@ -2489,7 +2489,7 @@ ${f.dpa.map((d) => " - " + d).join("\n")}`
2489
2489
  )
2490
2490
  }
2491
2491
  function startRun() {
2492
- getToken() ? (window.location.href = "/assessments") : openLogin()
2492
+ signedIn() ? (window.location.href = "/assessments") : openLogin()
2493
2493
  }
2494
2494
 
2495
2495
  function switchTab(name) {
@@ -2543,17 +2543,33 @@ ${f.dpa.map((d) => " - " + d).join("\n")}`
2543
2543
  return ""
2544
2544
  }
2545
2545
  }
2546
+ // The loopback dev sign-in establishes an httpOnly cookie session; no
2547
+ // bearer token is returned, so this flag tracks that it succeeded.
2548
+ function devSessionActive() {
2549
+ try {
2550
+ return localStorage.getItem("kifaru_dev_session") === "1"
2551
+ } catch (_) {
2552
+ return false
2553
+ }
2554
+ }
2555
+ function setDevSession(on) {
2556
+ try {
2557
+ on ? localStorage.setItem("kifaru_dev_session", "1") : localStorage.removeItem("kifaru_dev_session")
2558
+ } catch (_) {}
2559
+ }
2560
+ function signedIn() {
2561
+ return Boolean(getToken()) || devSessionActive()
2562
+ }
2546
2563
  async function autoDiscoverToken() {
2547
2564
  let t = getToken()
2548
- if (t) return t
2565
+ if (t || devSessionActive()) return t
2549
2566
  try {
2550
- const r = await fetch("/api/v1/auth/dev-token")
2567
+ // Session-scoped dev sign-in: the gateway sets an httpOnly session
2568
+ // cookie for loopback browsers and returns no token.
2569
+ const r = await fetch("/api/v1/auth/dev-token", { credentials: "include" })
2551
2570
  if (r.ok) {
2552
2571
  const d = await r.json()
2553
- if (d?.token) {
2554
- setToken(d.token)
2555
- return d.token
2556
- }
2572
+ if (d?.ok) setDevSession(true)
2557
2573
  }
2558
2574
  } catch (_) {}
2559
2575
  return ""
@@ -2565,7 +2581,7 @@ ${f.dpa.map((d) => " - " + d).join("\n")}`
2565
2581
  }
2566
2582
  function openLogin() {
2567
2583
  $("#loginErr").textContent = ""
2568
- $("#logoutBtn").hidden = !getToken()
2584
+ $("#logoutBtn").hidden = !signedIn()
2569
2585
  $("#loginModal").hidden = false
2570
2586
  setTimeout(() => $("#loginEmail").focus(), 30)
2571
2587
  }
@@ -2658,6 +2674,8 @@ ${f.dpa.map((d) => " - " + d).join("\n")}`
2658
2674
  }
2659
2675
  function signOut() {
2660
2676
  setToken("")
2677
+ setDevSession(false)
2678
+ fetch("/api/v1/auth/logout", { method: "POST", credentials: "include" }).catch(() => {})
2661
2679
  LIVE = null
2662
2680
  ACTIVE_RUN = null
2663
2681
  findingDetailCache.clear()
package/kifaru-gateway.js CHANGED
@@ -27012,6 +27012,7 @@ function createAuthApp(input2) {
27012
27012
  providers.github = createGitHubLoginProvider(input2.config.githubOAuth);
27013
27013
  }
27014
27014
  providers.local = createLocalProvider(input2.storage);
27015
+ providers.devlocal = createDevLocalProvider(input2.storage);
27015
27016
  return issuer({
27016
27017
  subjects,
27017
27018
  providers,
@@ -27022,7 +27023,7 @@ function createAuthApp(input2) {
27022
27023
  },
27023
27024
  async success(ctx, response) {
27024
27025
  const identity = await resolveIdentity(response);
27025
- if (!isAllowedEmail(identity.email, input2.config)) {
27026
+ if (response.provider !== "devlocal" && !isAllowedEmail(identity.email, input2.config)) {
27026
27027
  throw new Error(`Email ${identity.email} is not allowed for this alpha.`);
27027
27028
  }
27028
27029
  const role = identity.role ?? (response.provider === "bootstrap" && input2.config.bootstrapRole ? input2.config.bootstrapRole : input2.config.adminEmails.has(identity.email) ? "admin" : input2.config.defaultRole);
@@ -27145,7 +27146,7 @@ async function resolveIdentity(response) {
27145
27146
  name: readString(response.name)
27146
27147
  };
27147
27148
  }
27148
- if (response.provider === "local") {
27149
+ if (response.provider === "local" || response.provider === "devlocal") {
27149
27150
  return {
27150
27151
  subject: readString(response.subject) ?? response.email,
27151
27152
  email: String(response.email).toLowerCase(),
@@ -27465,6 +27466,30 @@ function createLocalProvider(storage) {
27465
27466
  }
27466
27467
  };
27467
27468
  }
27469
+ function createDevLocalProvider(storage) {
27470
+ return {
27471
+ type: "local",
27472
+ init() {},
27473
+ async client(input2) {
27474
+ const ticket = readString(input2.clientSecret);
27475
+ if (!ticket)
27476
+ throw new Error("Local dev ticket is required.");
27477
+ const stored = await storage.get(["local:devticket", ticket]);
27478
+ await storage.remove(["local:devticket", ticket]);
27479
+ if (!stored)
27480
+ throw new Error("Invalid or expired local dev ticket.");
27481
+ const email3 = readString(stored.email);
27482
+ if (!email3)
27483
+ throw new Error("Local dev ticket did not include an email.");
27484
+ return {
27485
+ subject: readString(stored.subject) ?? email3,
27486
+ email: email3,
27487
+ name: readString(stored.name),
27488
+ role: stored.role === "admin" || stored.role === "member" ? stored.role : undefined
27489
+ };
27490
+ }
27491
+ };
27492
+ }
27468
27493
  function isBootstrapToken(value, config2) {
27469
27494
  if (!value || !config2.bootstrapToken)
27470
27495
  return false;
@@ -27502,6 +27527,17 @@ async function createLocalTicket(storage, user) {
27502
27527
  }, new Date(Date.now() + 5 * 60 * 1000));
27503
27528
  return ticket;
27504
27529
  }
27530
+ var DEV_SESSION_EMAIL = "dev@kifaru.local";
27531
+ async function createDevLocalTicket(storage, user) {
27532
+ const ticket = `loc_${randomUUID2()}`;
27533
+ await storage.set(["local:devticket", ticket], {
27534
+ subject: `local:${user.email}`,
27535
+ email: user.email,
27536
+ name: user.name,
27537
+ role: user.role
27538
+ }, new Date(Date.now() + 60 * 1000));
27539
+ return ticket;
27540
+ }
27505
27541
  async function readLocalAuthPayload(request) {
27506
27542
  const contentType = request.headers.get("content-type") ?? "";
27507
27543
  if (contentType.includes("application/json")) {
@@ -31588,6 +31624,15 @@ function requireHumanAuth(config2, c) {
31588
31624
  }
31589
31625
  return null;
31590
31626
  }
31627
+ function isLoopbackPeer(c) {
31628
+ const server = c.env;
31629
+ try {
31630
+ const address = server?.requestIP?.(c.req.raw)?.address ?? "";
31631
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
31632
+ } catch {
31633
+ return false;
31634
+ }
31635
+ }
31591
31636
  function logGateway(level, category, message2) {
31592
31637
  const line = `${new Date().toISOString()} [${level}] [${category}] ${message2}`;
31593
31638
  console.log(line);
@@ -31675,28 +31720,70 @@ function createApp(input2) {
31675
31720
  bootstrapToken: input2.config.hasBootstrapToken,
31676
31721
  hasInternalToken: Boolean(input2.config.platformInternalToken)
31677
31722
  }));
31678
- app.get("/api/v1/auth/dev-token", (c) => {
31679
- const host = c.req.header("host") || "";
31680
- const isLoopback = host.startsWith("localhost") || host.startsWith("127.0.0.1") || host.startsWith("[::1]");
31681
- if (!isLoopback) {
31723
+ app.get("/api/v1/auth/dev-token", async (c) => {
31724
+ if (input2.config.hasOAuthProviders) {
31725
+ return c.json({ error: "OAuth sign-in is configured for this deployment." }, 403);
31726
+ }
31727
+ if (!isLoopbackPeer(c)) {
31682
31728
  return c.json({ error: "Forbidden" }, 403);
31683
31729
  }
31684
- const token = input2.config.platformInternalToken ?? null;
31685
- if (token) {
31686
- setCookie(c, "kifaru_access", token, {
31687
- httpOnly: false,
31688
- secure: false,
31730
+ let user = await getLocalUser(input2.storage, DEV_SESSION_EMAIL);
31731
+ if (!user) {
31732
+ user = await createLocalUser(input2.storage, {
31733
+ email: DEV_SESSION_EMAIL,
31734
+ password: `${randomUUID5()}${randomUUID5()}`,
31735
+ name: "Local Dev Session",
31736
+ role: "admin"
31737
+ });
31738
+ }
31739
+ const ticket = await createDevLocalTicket(input2.storage, user);
31740
+ const origin = input2.config.publicOrigin ?? publicOrigin(c.req.raw);
31741
+ const tokenResponse = await authFetch(new Request(`${origin}/token`, {
31742
+ method: "POST",
31743
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
31744
+ body: new URLSearchParams({
31745
+ grant_type: "client_credentials",
31746
+ provider: "devlocal",
31747
+ client_id: "kifaru-platform",
31748
+ client_secret: ticket
31749
+ }).toString()
31750
+ }));
31751
+ const tokenData = await tokenResponse.json().catch(() => ({}));
31752
+ const accessToken = readString(tokenData.access_token);
31753
+ if (!tokenResponse.ok || !accessToken) {
31754
+ return c.json({ error: "token_issuance_failed", detail: "Failed to issue session token." }, 500);
31755
+ }
31756
+ const csrfToken = randomUUID5();
31757
+ const isHttps = new URL(c.req.raw.url).protocol === "https:";
31758
+ const expiresIn = typeof tokenData.expires_in === "number" ? tokenData.expires_in : input2.config.accessTtlSeconds;
31759
+ setCookie(c, "kifaru_access", accessToken, {
31760
+ httpOnly: true,
31761
+ secure: isHttps,
31762
+ sameSite: "Lax",
31763
+ path: "/",
31764
+ maxAge: expiresIn
31765
+ });
31766
+ if (readString(tokenData.refresh_token)) {
31767
+ setCookie(c, "kifaru_refresh", String(tokenData.refresh_token), {
31768
+ httpOnly: true,
31769
+ secure: isHttps,
31689
31770
  sameSite: "Lax",
31690
- path: "/",
31691
- maxAge: 86400 * 7
31771
+ path: "/api/v1/auth",
31772
+ maxAge: Math.min(input2.config.refreshTtlSeconds, 30 * 86400)
31692
31773
  });
31693
31774
  }
31775
+ setCookie(c, "kifaru_csrf", csrfToken, {
31776
+ httpOnly: false,
31777
+ secure: isHttps,
31778
+ sameSite: "Lax",
31779
+ path: "/",
31780
+ maxAge: expiresIn
31781
+ });
31694
31782
  return c.json({
31695
- token,
31696
- workspace: {
31697
- id: input2.config.defaultWorkspaceId,
31698
- name: input2.config.defaultWorkspaceName
31699
- }
31783
+ ok: true,
31784
+ csrfToken,
31785
+ expiresInSeconds: expiresIn,
31786
+ user: { email: user.email, name: user.name, role: user.role }
31700
31787
  });
31701
31788
  });
31702
31789
  if (input2.config.staticRoot && existsSync2(input2.config.staticRoot)) {
@@ -33075,6 +33162,7 @@ import {
33075
33162
  defineTool,
33076
33163
  getAgentDir
33077
33164
  } from "@earendil-works/pi-coding-agent";
33165
+ import path6 from "path";
33078
33166
  // ../core/src/model/model-family.ts
33079
33167
  function getModelFamily(model) {
33080
33168
  const ref = typeof model === "string" ? model : `${model.providerID}/${model.modelID}`;
@@ -68857,6 +68945,9 @@ class WorkbenchAgentService {
68857
68945
  return this.adoptRuntime(id, runtime, actor, modelRef ?? null);
68858
68946
  }
68859
68947
  async resumeConversation(actor, sessionPath, modelRef) {
68948
+ if (!isSharedSessionPath(sessionPath)) {
68949
+ throw new Error("Session path is outside the shared session store.");
68950
+ }
68860
68951
  const id = `wb-${crypto.randomUUID().slice(0, 8)}`;
68861
68952
  const sessionManager = SessionManager.open(sessionPath, undefined, this.cwd);
68862
68953
  const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(id, modelRef ?? ""), {
@@ -68877,8 +68968,8 @@ class WorkbenchAgentService {
68877
68968
  tools: new Map,
68878
68969
  findingCount: 0,
68879
68970
  revision: 0,
68880
- dirty: true,
68881
68971
  runID: null,
68972
+ runCycle: 0,
68882
68973
  runPending: null,
68883
68974
  actor
68884
68975
  };
@@ -68890,6 +68981,9 @@ class WorkbenchAgentService {
68890
68981
  getConversation(id) {
68891
68982
  return this.conversations.get(id);
68892
68983
  }
68984
+ canAccess(conversation, actor) {
68985
+ return conversation.actor.workspaceID === actor.workspaceID;
68986
+ }
68893
68987
  refreshTitle(conversation) {
68894
68988
  const firstUser = conversation.session.messages.find((message2) => message2.role === "user");
68895
68989
  const text2 = firstUser && typeof firstUser.content === "string" ? firstUser.content : firstUser && Array.isArray(firstUser.content) ? firstUser.content.map((block) => ("text" in block) ? String(block.text) : "").join(" ").trim() : "";
@@ -68905,7 +68999,6 @@ class WorkbenchAgentService {
68905
68999
  status: "running",
68906
69000
  startedAt: Date.now()
68907
69001
  });
68908
- conversation.dirty = true;
68909
69002
  break;
68910
69003
  case "tool_execution_end": {
68911
69004
  const existing = conversation.tools.get(event.toolCallId);
@@ -68916,24 +69009,20 @@ class WorkbenchAgentService {
68916
69009
  startedAt: existing?.startedAt ?? Date.now(),
68917
69010
  durationMs: Date.now() - (existing?.startedAt ?? Date.now())
68918
69011
  });
68919
- conversation.dirty = true;
68920
69012
  break;
68921
69013
  }
68922
69014
  case "agent_start":
68923
69015
  this.ensurePlatformRun(conversation);
68924
- conversation.dirty = true;
68925
69016
  break;
68926
69017
  case "agent_end":
68927
69018
  this.completePlatformRun(conversation);
68928
69019
  this.refreshTitle(conversation);
68929
- conversation.dirty = true;
68930
69020
  break;
68931
69021
  case "message_start":
68932
69022
  case "message_update":
68933
69023
  case "message_end":
68934
69024
  case "turn_start":
68935
69025
  case "turn_end":
68936
- conversation.dirty = true;
68937
69026
  break;
68938
69027
  default:
68939
69028
  break;
@@ -68948,7 +69037,6 @@ class WorkbenchAgentService {
68948
69037
  ...conversation.session.isStreaming ? { deliverAs: "steer" } : {}
68949
69038
  });
68950
69039
  this.refreshTitle(conversation);
68951
- conversation.dirty = true;
68952
69040
  conversation.revision += 1;
68953
69041
  }
68954
69042
  snapshot(conversation) {
@@ -68973,7 +69061,6 @@ class WorkbenchAgentService {
68973
69061
  if (model && !conversation.session.isStreaming) {
68974
69062
  await conversation.session.setModel?.(model);
68975
69063
  }
68976
- conversation.dirty = true;
68977
69064
  conversation.revision += 1;
68978
69065
  }
68979
69066
  async ensurePlatformRun(conversation) {
@@ -68988,7 +69075,7 @@ class WorkbenchAgentService {
68988
69075
  kind: "hunt",
68989
69076
  title: `Live workspace \xB7 ${conversation.title}`.slice(0, 240),
68990
69077
  configuration: { surface: "workbench", cwd: this.cwd },
68991
- idempotencyKey: `workbench-${conversation.id}`
69078
+ idempotencyKey: `workbench-${conversation.id}-${conversation.runCycle}`
68992
69079
  });
68993
69080
  conversation.runID = run.id;
68994
69081
  this.platform.transitionRun(conversation.actor.workspaceID, run.id, "running");
@@ -69003,10 +69090,13 @@ class WorkbenchAgentService {
69003
69090
  }
69004
69091
  }
69005
69092
  completePlatformRun(conversation) {
69006
- if (!conversation.runID)
69093
+ const runID = conversation.runID;
69094
+ if (!runID)
69007
69095
  return;
69096
+ conversation.runID = null;
69097
+ conversation.runCycle += 1;
69008
69098
  try {
69009
- this.platform.transitionRun(conversation.actor.workspaceID, conversation.runID, "completed", {
69099
+ this.platform.transitionRun(conversation.actor.workspaceID, runID, "completed", {
69010
69100
  summary: { findings: conversation.findingCount, surface: "workbench" }
69011
69101
  });
69012
69102
  } catch (error92) {
@@ -69031,7 +69121,7 @@ class WorkbenchAgentService {
69031
69121
  }
69032
69122
  const p = params;
69033
69123
  const finding = {
69034
- fingerprint: (p.fingerprint?.trim() || `workbench:${slugify2(p.title)}`).slice(0, 512),
69124
+ fingerprint: (p.fingerprint?.trim() || `workbench:${slugify2(p.asset)}:${slugify2(p.title)}`).slice(0, 512),
69035
69125
  ruleID: (p.weakness?.trim() || slugify2(p.title) || "finding").slice(0, 160),
69036
69126
  title: p.title.slice(0, 256),
69037
69127
  ...p.description ? { description: p.description.slice(0, 20000) } : {},
@@ -69073,6 +69163,11 @@ class WorkbenchAgentService {
69073
69163
  });
69074
69164
  }
69075
69165
  }
69166
+ function isSharedSessionPath(sessionPath) {
69167
+ const resolved = path6.resolve(sessionPath);
69168
+ const root = path6.resolve(getAgentDir(), "sessions");
69169
+ return resolved.endsWith(".jsonl") && resolved.startsWith(root + path6.sep);
69170
+ }
69076
69171
  function slugify2(value) {
69077
69172
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
69078
69173
  }
@@ -69104,6 +69199,7 @@ var reportFindingSchema = exports_typebox.Object({
69104
69199
 
69105
69200
  // src/workbench/ws.ts
69106
69201
  var SNAPSHOT_INTERVAL_MS = 150;
69202
+ var FOREIGN_WORKSPACE_NOTICE = "That conversation belongs to another workspace.";
69107
69203
  function workbenchSocketHandlers(service) {
69108
69204
  function send(socket, message2) {
69109
69205
  try {
@@ -69115,17 +69211,33 @@ function workbenchSocketHandlers(service) {
69115
69211
  clearInterval(data.timer);
69116
69212
  data.timer = setInterval(() => {
69117
69213
  const conversation = data.conversationId ? service.getConversation(data.conversationId) : undefined;
69118
- if (!conversation || !conversation.dirty)
69214
+ if (!conversation)
69215
+ return;
69216
+ const revision = conversation.revision;
69217
+ if (revision <= (data.lastRevision ?? 0))
69119
69218
  return;
69120
- conversation.dirty = false;
69219
+ data.lastRevision = revision;
69121
69220
  send(socket, service.snapshot(conversation));
69122
69221
  }, SNAPSHOT_INTERVAL_MS);
69123
69222
  }
69124
69223
  function attach(socket, data, conversation) {
69125
69224
  data.conversationId = conversation.id;
69225
+ data.lastRevision = conversation.revision;
69126
69226
  send(socket, service.snapshot(conversation));
69127
69227
  pump(socket, data);
69128
69228
  }
69229
+ function governedConversation(data, onForeign, onMissing) {
69230
+ const conversation = data.conversationId ? service.getConversation(data.conversationId) : undefined;
69231
+ if (!conversation) {
69232
+ onMissing();
69233
+ return;
69234
+ }
69235
+ if (!service.canAccess(conversation, data.actor)) {
69236
+ onForeign();
69237
+ return;
69238
+ }
69239
+ return conversation;
69240
+ }
69129
69241
  return {
69130
69242
  open() {},
69131
69243
  async message(socket, raw2) {
@@ -69141,6 +69253,10 @@ function workbenchSocketHandlers(service) {
69141
69253
  switch (message2.type) {
69142
69254
  case "attach": {
69143
69255
  const existing = message2.conversationId ? service.getConversation(message2.conversationId) : undefined;
69256
+ if (existing && !service.canAccess(existing, data.actor)) {
69257
+ send(socket, { type: "notice", level: "error", text: FOREIGN_WORKSPACE_NOTICE });
69258
+ return;
69259
+ }
69144
69260
  const conversation = existing ?? await service.createConversation(data.actor);
69145
69261
  attach(socket, data, conversation);
69146
69262
  send(socket, { type: "models", models: service.listModels() });
@@ -69162,16 +69278,14 @@ function workbenchSocketHandlers(service) {
69162
69278
  }
69163
69279
  case "prompt":
69164
69280
  case "steer": {
69165
- const conversation = data.conversationId ? service.getConversation(data.conversationId) : undefined;
69166
- if (!conversation) {
69167
- send(socket, { type: "notice", level: "error", text: "No conversation attached" });
69281
+ const conversation = governedConversation(data, () => send(socket, { type: "notice", level: "error", text: FOREIGN_WORKSPACE_NOTICE }), () => send(socket, { type: "notice", level: "error", text: "No conversation attached" }));
69282
+ if (!conversation)
69168
69283
  return;
69169
- }
69170
69284
  await service.prompt(conversation, message2.text);
69171
69285
  break;
69172
69286
  }
69173
69287
  case "select_model": {
69174
- const conversation = data.conversationId ? service.getConversation(data.conversationId) : undefined;
69288
+ const conversation = governedConversation(data, () => send(socket, { type: "notice", level: "error", text: FOREIGN_WORKSPACE_NOTICE }), () => {});
69175
69289
  if (conversation)
69176
69290
  await service.selectModel(conversation, message2.model);
69177
69291
  break;
package/kifaru-pi.js CHANGED
@@ -16476,7 +16476,7 @@ var init_installation = __esm(async () => {
16476
16476
  });
16477
16477
  }
16478
16478
  Installation.upgrade = upgrade;
16479
- Installation.VERSION = "1.0.177";
16479
+ Installation.VERSION = "1.0.178";
16480
16480
  Installation.CHANNEL = typeof KIFARU_CHANNEL === "string" ? KIFARU_CHANNEL : "local";
16481
16481
  Installation.USER_AGENT = `kifaru/${Installation.CHANNEL}/${Installation.VERSION}`;
16482
16482
  async function latest() {
@@ -133185,7 +133185,7 @@ Ctrl+C stops the gateway.`);
133185
133185
  async function handleCli() {
133186
133186
  const args = process.argv.slice(2);
133187
133187
  if (args.includes("--version") || args.includes("-v")) {
133188
- console.log("1.0.177");
133188
+ console.log("1.0.178");
133189
133189
  process.exit(0);
133190
133190
  }
133191
133191
  if (args.includes("--help") || args.includes("-h")) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kifaru",
3
- "version": "1.0.177",
4
- "gitHead": "3ff233ae1832c96ef45583d460e265e266495bb9",
3
+ "version": "1.0.178",
4
+ "gitHead": "6918585a066d299c117d06522515ad010e37e688",
5
5
  "description": "Kifaru security agent on the Pi runtime",
6
6
  "type": "module",
7
7
  "license": "MIT",