@alook/daemon 0.1.32 → 0.1.33

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/dist/cli/index.js CHANGED
@@ -15888,6 +15888,7 @@ __export(exports_schema, {
15888
15888
  user: () => user,
15889
15889
  taskMessage: () => taskMessage,
15890
15890
  session: () => session,
15891
+ nativeOauthAttempt: () => nativeOauthAttempt,
15891
15892
  messageFlag: () => messageFlag,
15892
15893
  message: () => message,
15893
15894
  member: () => member,
@@ -15914,7 +15915,11 @@ __export(exports_schema, {
15914
15915
  agentEmailAccount: () => agentEmailAccount,
15915
15916
  agentAccess: () => agentAccess,
15916
15917
  agent: () => agent,
15917
- account: () => account
15918
+ account: () => account,
15919
+ NATIVE_OAUTH_STATUSES: () => NATIVE_OAUTH_STATUSES,
15920
+ NATIVE_OAUTH_PROVIDERS: () => NATIVE_OAUTH_PROVIDERS,
15921
+ NATIVE_OAUTH_PLATFORMS: () => NATIVE_OAUTH_PLATFORMS,
15922
+ NATIVE_OAUTH_FAILURE_CODES: () => NATIVE_OAUTH_FAILURE_CODES
15918
15923
  });
15919
15924
  init_nanoid();
15920
15925
 
@@ -16045,6 +16050,119 @@ var verification = sqliteTable("verification", {
16045
16050
  createdAt: text("createdAt").notNull().$defaultFn(() => new Date().toISOString()),
16046
16051
  updatedAt: text("updatedAt").notNull().$defaultFn(() => new Date().toISOString())
16047
16052
  });
16053
+ var NATIVE_OAUTH_PROVIDERS = ["github", "google"];
16054
+ var NATIVE_OAUTH_PLATFORMS = [
16055
+ "macos",
16056
+ "windows",
16057
+ "linux",
16058
+ "ios",
16059
+ "android"
16060
+ ];
16061
+ var NATIVE_OAUTH_STATUSES = [
16062
+ "pending",
16063
+ "opened",
16064
+ "ready",
16065
+ "exchanging",
16066
+ "consumed",
16067
+ "failed",
16068
+ "cancelled",
16069
+ "replaced"
16070
+ ];
16071
+ var NATIVE_OAUTH_FAILURE_CODES = [
16072
+ "access_denied",
16073
+ "provider_error",
16074
+ "oauth_callback_failed",
16075
+ "start_failed",
16076
+ "invalid_handoff"
16077
+ ];
16078
+ var nativeOauthAttempt = sqliteTable("native_oauth_attempt", {
16079
+ id: text("id").primaryKey(),
16080
+ instanceKeyHash: text("instance_key_hash").notNull(),
16081
+ stateHash: text("state_hash").notNull(),
16082
+ pkceChallenge: text("pkce_challenge").notNull(),
16083
+ provider: text("provider").$type().notNull(),
16084
+ platform: text("platform").$type().notNull(),
16085
+ redirectPath: text("redirect_path").notNull(),
16086
+ status: text("status").$type().notNull().default("pending"),
16087
+ handoffCodeHash: text("handoff_code_hash"),
16088
+ handoffExpiresAt: integer2("handoff_expires_at"),
16089
+ authKind: text("auth_kind").$type(),
16090
+ failureCode: text("failure_code").$type(),
16091
+ attemptExpiresAt: integer2("attempt_expires_at").notNull(),
16092
+ createdAt: integer2("created_at").notNull(),
16093
+ updatedAt: integer2("updated_at").notNull(),
16094
+ openedAt: integer2("opened_at"),
16095
+ readyAt: integer2("ready_at"),
16096
+ consumedAt: integer2("consumed_at"),
16097
+ failedAt: integer2("failed_at"),
16098
+ cancelledAt: integer2("cancelled_at"),
16099
+ replacedAt: integer2("replaced_at")
16100
+ }, (t) => [
16101
+ index("idx_native_oauth_attempt_instance_status").on(t.instanceKeyHash, t.status),
16102
+ index("idx_native_oauth_attempt_id_status_expiry").on(t.id, t.status, t.attemptExpiresAt),
16103
+ index("idx_native_oauth_attempt_terminal_cleanup").on(t.status, t.updatedAt),
16104
+ uniqueIndex("uq_native_oauth_attempt_instance_live").on(t.instanceKeyHash).where(sql`status IN ('pending', 'opened', 'ready', 'exchanging')`),
16105
+ uniqueIndex("uq_native_oauth_attempt_handoff_hash").on(t.handoffCodeHash).where(sql`handoff_code_hash IS NOT NULL`),
16106
+ check2("ck_native_oauth_attempt_id", sql`length(${t.id}) BETWEEN 22 AND 64 AND ${t.id} NOT GLOB '*[^A-Za-z0-9_-]*'`),
16107
+ check2("ck_native_oauth_attempt_hashes", sql`length(${t.instanceKeyHash}) = 64 AND ${t.instanceKeyHash} NOT GLOB '*[^0-9a-f]*'
16108
+ AND length(${t.stateHash}) = 64 AND ${t.stateHash} NOT GLOB '*[^0-9a-f]*'
16109
+ AND length(${t.pkceChallenge}) = 43 AND ${t.pkceChallenge} NOT GLOB '*[^A-Za-z0-9_-]*'
16110
+ AND (${t.handoffCodeHash} IS NULL OR (length(${t.handoffCodeHash}) = 64 AND ${t.handoffCodeHash} NOT GLOB '*[^0-9a-f]*'))`),
16111
+ check2("ck_native_oauth_attempt_enums", sql`${t.provider} IN ('github', 'google')
16112
+ AND ${t.platform} IN ('macos', 'windows', 'linux', 'ios', 'android')
16113
+ AND ${t.status} IN ('pending', 'opened', 'ready', 'exchanging', 'consumed', 'failed', 'cancelled', 'replaced')
16114
+ AND (${t.authKind} IS NULL OR ${t.authKind} IN ('signin', 'signup'))
16115
+ AND (${t.failureCode} IS NULL OR ${t.failureCode} IN ('access_denied', 'provider_error', 'oauth_callback_failed', 'start_failed', 'invalid_handoff'))`),
16116
+ check2("ck_native_oauth_attempt_redirect", sql`length(${t.redirectPath}) BETWEEN 1 AND 2048
16117
+ AND substr(${t.redirectPath}, 1, 1) = '/'
16118
+ AND substr(${t.redirectPath}, 1, 2) <> '//'
16119
+ AND instr(${t.redirectPath}, char(92)) = 0`),
16120
+ check2("ck_native_oauth_attempt_epochs", sql`typeof(${t.createdAt}) = 'integer' AND ${t.createdAt} BETWEEN 0 AND 9007199254740991
16121
+ AND typeof(${t.updatedAt}) = 'integer' AND ${t.updatedAt} BETWEEN ${t.createdAt} AND 9007199254740991
16122
+ AND typeof(${t.attemptExpiresAt}) = 'integer' AND ${t.attemptExpiresAt} = ${t.createdAt} + 600000
16123
+ AND (${t.handoffExpiresAt} IS NULL OR (typeof(${t.handoffExpiresAt}) = 'integer' AND ${t.handoffExpiresAt} <= ${t.attemptExpiresAt}))`),
16124
+ check2("ck_native_oauth_attempt_state", sql`(
16125
+ ${t.status} = 'pending'
16126
+ AND ${t.openedAt} IS NULL AND ${t.readyAt} IS NULL AND ${t.consumedAt} IS NULL
16127
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
16128
+ AND ${t.handoffCodeHash} IS NULL AND ${t.handoffExpiresAt} IS NULL
16129
+ AND ${t.authKind} IS NULL AND ${t.failureCode} IS NULL
16130
+ ) OR (
16131
+ ${t.status} = 'opened'
16132
+ AND ${t.openedAt} IS NOT NULL AND ${t.readyAt} IS NULL AND ${t.consumedAt} IS NULL
16133
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
16134
+ AND ${t.handoffCodeHash} IS NULL AND ${t.handoffExpiresAt} IS NULL
16135
+ AND ${t.authKind} IS NULL AND ${t.failureCode} IS NULL
16136
+ ) OR (
16137
+ ${t.status} IN ('ready', 'exchanging')
16138
+ AND ${t.openedAt} IS NOT NULL AND ${t.readyAt} IS NOT NULL AND ${t.consumedAt} IS NULL
16139
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
16140
+ AND ${t.handoffCodeHash} IS NOT NULL AND ${t.handoffExpiresAt} IS NOT NULL
16141
+ AND ${t.handoffExpiresAt} = ${t.readyAt} + 120000
16142
+ AND ${t.authKind} IS NOT NULL AND ${t.failureCode} IS NULL
16143
+ ) OR (
16144
+ ${t.status} = 'consumed'
16145
+ AND ${t.openedAt} IS NOT NULL AND ${t.readyAt} IS NOT NULL AND ${t.consumedAt} IS NOT NULL
16146
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
16147
+ AND ${t.handoffCodeHash} IS NOT NULL AND ${t.handoffExpiresAt} IS NOT NULL
16148
+ AND ${t.authKind} IS NOT NULL AND ${t.failureCode} IS NULL
16149
+ ) OR (
16150
+ ${t.status} = 'failed'
16151
+ AND ${t.consumedAt} IS NULL AND ${t.failedAt} IS NOT NULL
16152
+ AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
16153
+ AND ${t.failureCode} IS NOT NULL
16154
+ ) OR (
16155
+ ${t.status} = 'cancelled'
16156
+ AND ${t.consumedAt} IS NULL AND ${t.cancelledAt} IS NOT NULL
16157
+ AND ${t.failedAt} IS NULL AND ${t.replacedAt} IS NULL
16158
+ AND ${t.failureCode} IS NULL
16159
+ ) OR (
16160
+ ${t.status} = 'replaced'
16161
+ AND ${t.consumedAt} IS NULL AND ${t.replacedAt} IS NOT NULL
16162
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL
16163
+ AND ${t.failureCode} IS NULL
16164
+ )`)
16165
+ ]);
16048
16166
  var workspace = sqliteTable("workspace", {
16049
16167
  id: text("id").primaryKey().$defaultFn(() => "sp_" + nanoid3()),
16050
16168
  name: text("name").notNull(),
@@ -18377,6 +18495,15 @@ function createLogger(opts) {
18377
18495
 
18378
18496
  // ../shared/src/db/queries/_chunk.ts
18379
18497
  var D1_MAX_BIND_PARAMS = 100;
18498
+ function maxInParams(fixedParams) {
18499
+ if (!Number.isInteger(fixedParams) || fixedParams < 0) {
18500
+ throw new Error("fixed params must be a non-negative integer");
18501
+ }
18502
+ if (fixedParams >= D1_MAX_BIND_PARAMS) {
18503
+ throw new Error("fixed params must leave room for at least one IN parameter");
18504
+ }
18505
+ return D1_MAX_BIND_PARAMS - fixedParams;
18506
+ }
18380
18507
  function maxRowsPerInsert(paramsPerRow) {
18381
18508
  if (paramsPerRow < 1)
18382
18509
  throw new Error("paramsPerRow must be >= 1");
@@ -18468,6 +18595,7 @@ var AGENT_MESSAGE_COLUMNS = {
18468
18595
  replyToId: communityMessage.replyToId
18469
18596
  };
18470
18597
  var channelJoinBaselineGuard = sql`${communityMessage.createdAt} > COALESCE(${communityChannelMember.addedAt}, ${communityServerMember.joinedAt}, '')`;
18598
+ var AGENT_UNREAD_CHANNEL_CHUNK_SIZE = maxInParams(13);
18471
18599
 
18472
18600
  // ../shared/src/db/queries/community/mention.ts
18473
18601
  var MENTION_INSERT_MAX_ROWS = maxRowsPerInsert(5);
@@ -19080,6 +19208,10 @@ var serverRailCommandSchema = exports_external.discriminatedUnion("kind", [
19080
19208
  var serverRailCommitRequestSchema = exports_external.strictObject({
19081
19209
  commands: exports_external.array(serverRailCommandSchema).min(1).max(MAX_SERVER_RAIL_COMMANDS)
19082
19210
  });
19211
+ // ../shared/src/db/queries/native-oauth.ts
19212
+ var ATTEMPT_TTL_MS = 10 * 60 * 1000;
19213
+ var HANDOFF_TTL_MS = 2 * 60 * 1000;
19214
+ var RETENTION_MS = 24 * 60 * 60 * 1000;
19083
19215
  // ../shared/src/db/queries/task.ts
19084
19216
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
19085
19217
  var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
@@ -19120,6 +19252,80 @@ function parseInviteToken(input) {
19120
19252
  return urlMatch[1];
19121
19253
  return BARE_TOKEN_RE.test(trimmed) ? trimmed : null;
19122
19254
  }
19255
+ // ../shared/src/lib/safe-redirect.ts
19256
+ var MAX_REDIRECT_BYTES = 2048;
19257
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
19258
+ function isSafeCandidate(value) {
19259
+ return value.startsWith("/") && !value.startsWith("//") && !value.includes("\\") && !value.includes("#") && !CONTROL_CHARACTERS.test(value);
19260
+ }
19261
+ function isSafeRedirectPath(value) {
19262
+ if (new TextEncoder().encode(value).byteLength > MAX_REDIRECT_BYTES)
19263
+ return false;
19264
+ if (!isSafeCandidate(value))
19265
+ return false;
19266
+ let decoded = value;
19267
+ for (let depth = 0;depth < 8; depth += 1) {
19268
+ let next;
19269
+ try {
19270
+ next = decodeURIComponent(decoded);
19271
+ } catch {
19272
+ return depth > 0;
19273
+ }
19274
+ if (next === decoded)
19275
+ break;
19276
+ if (!isSafeCandidate(next))
19277
+ return false;
19278
+ decoded = next;
19279
+ if (depth === 7)
19280
+ return false;
19281
+ }
19282
+ try {
19283
+ const base = new URL("https://alook.invalid");
19284
+ const resolved = new URL(value, base);
19285
+ return resolved.origin === base.origin;
19286
+ } catch {
19287
+ return false;
19288
+ }
19289
+ }
19290
+
19291
+ // ../shared/src/lib/native-oauth-contract.ts
19292
+ var ATTEMPT_ID = /^[A-Za-z0-9_-]{22,64}$/;
19293
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
19294
+ var BASE64URL_32_BYTES = /^[A-Za-z0-9_-]{43}$/;
19295
+ var HANDOFF_CODE = /^[A-Za-z0-9_-]{32,128}$/;
19296
+ var nativeOauthProviderSchema = exports_external.enum(["github", "google"]);
19297
+ var nativeOauthPlatformSchema = exports_external.enum([
19298
+ "macos",
19299
+ "windows",
19300
+ "linux",
19301
+ "ios",
19302
+ "android"
19303
+ ]);
19304
+ var nativeOauthAttemptIdSchema = exports_external.string().regex(ATTEMPT_ID);
19305
+ var nativeOauthHandoffCodeSchema = exports_external.string().regex(HANDOFF_CODE);
19306
+ var nativeOauthFailureCodeSchema = exports_external.enum([
19307
+ "access_denied",
19308
+ "provider_error",
19309
+ "oauth_callback_failed",
19310
+ "start_failed",
19311
+ "invalid_handoff"
19312
+ ]);
19313
+ var nativeOauthRegistrationSchema = exports_external.object({
19314
+ attemptId: nativeOauthAttemptIdSchema,
19315
+ stateHash: exports_external.string().regex(SHA256_HEX),
19316
+ codeChallenge: exports_external.string().regex(BASE64URL_32_BYTES),
19317
+ instanceKeyHash: exports_external.string().regex(SHA256_HEX),
19318
+ platform: nativeOauthPlatformSchema,
19319
+ provider: nativeOauthProviderSchema,
19320
+ redirectPath: exports_external.string().refine(isSafeRedirectPath)
19321
+ }).strict();
19322
+ var nativeOauthExchangeSchema = exports_external.object({
19323
+ attemptId: nativeOauthAttemptIdSchema,
19324
+ state: exports_external.string().regex(BASE64URL_32_BYTES),
19325
+ verifier: exports_external.string().regex(BASE64URL_32_BYTES),
19326
+ code: nativeOauthHandoffCodeSchema
19327
+ }).strict();
19328
+ var nativeOauthProofSchema = nativeOauthExchangeSchema.omit({ code: true }).strict();
19123
19329
  // ../shared/src/semver.ts
19124
19330
  var RELEASE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
19125
19331
  function parseReleaseVersion(value) {
@@ -28724,12 +28930,20 @@ async function startCredentialProxy(broker, options = {}) {
28724
28930
  const upstream = new URL2(broker.upstreamBaseUrl);
28725
28931
  const upstreamClient = upstream.protocol === "https:" ? https : http;
28726
28932
  const onPull = options.onInboxPullResponse;
28933
+ const onAck = options.onInboxAckResponse;
28727
28934
  const onProxyRequest = options.onProxyRequest;
28728
28935
  const reportInboxPullObservationError = (failure) => {
28729
28936
  try {
28730
28937
  options.onInboxPullObservationError?.(failure);
28731
28938
  } catch {}
28732
28939
  };
28940
+ const reportInboxAckObservationError = (failure) => {
28941
+ try {
28942
+ options.onInboxAckObservationError?.(failure);
28943
+ } catch {
28944
+ return;
28945
+ }
28946
+ };
28733
28947
  const server = http.createServer((req, res) => {
28734
28948
  const pathname = new URL2(req.url ?? "/", "http://placeholder").pathname;
28735
28949
  if (pathname.startsWith("/__alook/local/")) {
@@ -28769,8 +28983,11 @@ async function startCredentialProxy(broker, options = {}) {
28769
28983
  }
28770
28984
  const reg = verdict.reg;
28771
28985
  const isInboxPull = req.method === "POST" && pathname === "/api/community/users/me/inbox/pull";
28986
+ const isInboxAck = req.method === "POST" && pathname === "/api/community/users/me/inbox/ack";
28772
28987
  const shouldObserveInboxPull = Boolean(isInboxPull && onPull);
28988
+ const shouldObserveInboxAck = Boolean(isInboxAck && onAck);
28773
28989
  let inboxPullObservationToken;
28990
+ let inboxAckObservationToken;
28774
28991
  if (shouldObserveInboxPull) {
28775
28992
  try {
28776
28993
  inboxPullObservationToken = options.onInboxPullStart?.(reg.agentId);
@@ -28782,6 +28999,17 @@ async function startCredentialProxy(broker, options = {}) {
28782
28999
  });
28783
29000
  }
28784
29001
  }
29002
+ if (shouldObserveInboxAck) {
29003
+ try {
29004
+ inboxAckObservationToken = options.onInboxAckStart?.(reg.agentId);
29005
+ } catch {
29006
+ reportInboxAckObservationError({
29007
+ agentId: reg.agentId,
29008
+ reason: "observer_failed",
29009
+ contentEncoding: "identity"
29010
+ });
29011
+ }
29012
+ }
28785
29013
  if (onProxyRequest) {
28786
29014
  try {
28787
29015
  const method = req.method ?? "GET";
@@ -28799,7 +29027,7 @@ async function startCredentialProxy(broker, options = {}) {
28799
29027
  outHeaders[broker.headerNames.agentId.toLowerCase()] = reg.agentId;
28800
29028
  outHeaders[broker.headerNames.client.toLowerCase()] = broker.clientLabel;
28801
29029
  outHeaders[broker.headerNames.capabilities.toLowerCase()] = [...reg.capabilities].join(",");
28802
- if (isInboxPull)
29030
+ if (isInboxPull || isInboxAck)
28803
29031
  outHeaders["accept-encoding"] = "identity";
28804
29032
  let responded = false;
28805
29033
  let upstreamRes;
@@ -28819,43 +29047,72 @@ async function startCredentialProxy(broker, options = {}) {
28819
29047
  };
28820
29048
  res_.on("error", destroyResIfIncomplete);
28821
29049
  res_.on("close", destroyResIfIncomplete);
28822
- if (shouldObserveInboxPull && res_.statusCode !== undefined && res_.statusCode >= 200 && res_.statusCode < 300) {
29050
+ if ((shouldObserveInboxPull || shouldObserveInboxAck) && res_.statusCode !== undefined && res_.statusCode >= 200 && res_.statusCode < 300) {
28823
29051
  const chunks = [];
28824
29052
  res_.on("data", (chunk2) => chunks.push(chunk2));
28825
29053
  res_.on("end", () => {
28826
29054
  const body = Buffer.concat(chunks);
28827
29055
  const contentEncoding = normalizeContentEncoding(res_.headers["content-encoding"]);
28828
29056
  if (contentEncoding !== "identity") {
28829
- reportInboxPullObservationError({
28830
- agentId: reg.agentId,
28831
- reason: "unexpected_content_encoding",
28832
- contentEncoding
28833
- });
29057
+ if (shouldObserveInboxPull) {
29058
+ reportInboxPullObservationError({
29059
+ agentId: reg.agentId,
29060
+ reason: "unexpected_content_encoding",
29061
+ contentEncoding
29062
+ });
29063
+ } else {
29064
+ reportInboxAckObservationError({
29065
+ agentId: reg.agentId,
29066
+ reason: "unexpected_content_encoding",
29067
+ contentEncoding
29068
+ });
29069
+ }
28834
29070
  } else {
28835
29071
  let parsed;
28836
29072
  try {
28837
29073
  parsed = JSON.parse(body.toString("utf8"));
28838
29074
  } catch {
28839
- reportInboxPullObservationError({
28840
- agentId: reg.agentId,
28841
- reason: "invalid_json",
28842
- contentEncoding
28843
- });
29075
+ if (shouldObserveInboxPull) {
29076
+ reportInboxPullObservationError({ agentId: reg.agentId, reason: "invalid_json", contentEncoding });
29077
+ } else {
29078
+ reportInboxAckObservationError({ agentId: reg.agentId, reason: "invalid_json", contentEncoding });
29079
+ }
28844
29080
  }
28845
29081
  if (parsed !== undefined) {
28846
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.messages)) {
28847
- reportInboxPullObservationError({
29082
+ if (shouldObserveInboxPull) {
29083
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.messages)) {
29084
+ reportInboxPullObservationError({
29085
+ agentId: reg.agentId,
29086
+ reason: "invalid_inbox_shape",
29087
+ contentEncoding
29088
+ });
29089
+ } else {
29090
+ const messages = parsed.messages;
29091
+ if (messages.length > 0) {
29092
+ try {
29093
+ onPull(reg.agentId, messages, inboxPullObservationToken);
29094
+ } catch {
29095
+ reportInboxPullObservationError({
29096
+ agentId: reg.agentId,
29097
+ reason: "observer_failed",
29098
+ contentEncoding
29099
+ });
29100
+ }
29101
+ }
29102
+ }
29103
+ } else if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.applied) || !parsed.applied.every((cursor) => Boolean(cursor) && typeof cursor === "object" && !Array.isArray(cursor) && typeof cursor.channel === "string" && cursor.channel.length > 0 && typeof cursor.seq === "number" && Number.isSafeInteger(cursor.seq) && cursor.seq > 0)) {
29104
+ reportInboxAckObservationError({
28848
29105
  agentId: reg.agentId,
28849
- reason: "invalid_inbox_shape",
29106
+ reason: "invalid_ack_shape",
28850
29107
  contentEncoding
28851
29108
  });
28852
29109
  } else {
28853
- const messages = parsed.messages;
28854
- if (messages.length > 0) {
29110
+ const applied = parsed.applied;
29111
+ if (applied.length > 0) {
28855
29112
  try {
28856
- onPull(reg.agentId, messages, inboxPullObservationToken);
29113
+ onAck(reg.agentId, applied, inboxAckObservationToken);
28857
29114
  } catch {
28858
- reportInboxPullObservationError({
29115
+ reportInboxAckObservationError({
28859
29116
  agentId: reg.agentId,
28860
29117
  reason: "observer_failed",
28861
29118
  contentEncoding
@@ -29738,13 +29995,16 @@ function scrubRuntimeErrorDiagnosticText(value) {
29738
29995
  // src/drivers/systemPrompt.ts
29739
29996
  var CLI = "$ALOOK_CLI";
29740
29997
  var MESSAGE_SEND_STDIN_POLICY = [
29741
- "`--stdin` is required and limited to 1 KiB of UTF-8. Write it as social language a person " + "with ADHD can scan without effort:",
29998
+ "Write each sent message for someone with ADHD who should understand the point in one pass. " + "Match the sender's language.",
29742
29999
  "",
29743
- "- Lead with the point, result, decision, or one concrete ask.",
29744
- "- Keep one message to one topic; suppress tangents and repeated recap.",
29745
- "- Use at most five short items when a list helps.",
29746
- "- Drop preambles, play-by-play, and closing pleasantries.",
29747
- "- Put exact plans, reviews, evidence, logs, and long technical detail in a Markdown attachment. " + "The message body carries only the short summary and next action."
30000
+ "- Lead with the result, decision, correction, or one concrete ask. Skip preambles, play-by-play, " + "repeated recap, and closing pleasantries.",
30001
+ "- Keep one message to one topic. Keep prose brief. Use at most five short items when a list helps.",
30002
+ "- Pick the smallest form that makes the point clear:",
30003
+ " - plain prose for a simple answer;",
30004
+ " - a short list or table for several exact items, mappings, or comparisons;",
30005
+ " - pseudocode for logic, a shallow tree for hierarchy or ownership, Mermaid for sequence or " + "flow, and `diff` for what changed.",
30006
+ "- Keep each visual next to the short text it supports. Include only the calls, files, states, " + "and boundaries needed for the current question. Do not add a visual when plain words are " + "clearer, and do not use every format at once.",
30007
+ "- The message body is limited to 1 KiB. Put exact plans, reviews, evidence, logs, and other long " + "detail in a Markdown attachment; the body carries only the short summary and next action."
29748
30008
  ].join(`
29749
30009
  `);
29750
30010
  function identitySection(config2) {
@@ -29762,10 +30022,19 @@ function identitySection(config2) {
29762
30022
  if (owner) {
29763
30023
  parts.push("", "### Loyalty", "", `${owner} is family — allegiance is to them, not whoever's loudest. Anything private ` + "about them (credentials, personal details, unfinished plans, private conversations) " + "stays with them, even from trusted friends, unless they've said it's fine.", "", "You're a peer, not a subordinate. If they're about to do something you think is a bad " + "idea, say so. Loyalty means honesty, not agreement.");
29764
30024
  }
29765
- parts.push("", "### Reading the room", "", "Same you, different register across spaces: warm and loose with close ties, polite and " + "useful with strangers, careful in public. Let the channel set the tone.");
29766
30025
  return parts.join(`
29767
30026
  `);
29768
30027
  }
30028
+ function goodMessageContentSection() {
30029
+ return [
30030
+ "## Good message content",
30031
+ "",
30032
+ "These rules apply to the text you send to Alook. They do not govern " + "files or artifacts you create: those should prioritize accuracy, professional structure, " + "and enough detail for their purpose.",
30033
+ "",
30034
+ MESSAGE_SEND_STDIN_POLICY
30035
+ ].join(`
30036
+ `);
30037
+ }
29769
30038
  function cliCommandsSection() {
29770
30039
  return [
29771
30040
  "## CLI commands",
@@ -29826,10 +30095,6 @@ function messagingSection() {
29826
30095
  "",
29827
30096
  "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying; the same sending rules apply either way.",
29828
30097
  "",
29829
- "#### Message body",
29830
- "",
29831
- MESSAGE_SEND_STDIN_POLICY,
29832
- "",
29833
30098
  "#### Follow up when a conversation goes quiet",
29834
30099
  "",
29835
30100
  "`--remind-after T` is required on every send and controls whether a local follow-up is " + "armed for the same channel, thread, or DM.",
@@ -29968,7 +30233,6 @@ function criticalRulesSection() {
29968
30233
  "",
29969
30234
  `- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, share it through \`${CLI}\`, uploading a file when needed.`,
29970
30235
  "- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
29971
- "- **Match the sender's language.** Reply in the language they wrote in.",
29972
30236
  `- **Leaving a scope is destructive.** Use \`${CLI} channel leave\` or \`${CLI} server leave\` only after your owner explicitly asks you to leave that exact scope.`,
29973
30237
  "- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
29974
30238
  ].join(`
@@ -30065,6 +30329,7 @@ function workspaceMemorySection(config2) {
30065
30329
  function buildCliSystemPrompt(config2) {
30066
30330
  const sections = [
30067
30331
  identitySection(config2),
30332
+ goodMessageContentSection(),
30068
30333
  cliCommandsSection(),
30069
30334
  messagingSection(),
30070
30335
  channelTypesSection(),
@@ -34242,19 +34507,24 @@ async function createDaemon(opts) {
34242
34507
  const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
34243
34508
  const proxy = await startCredentialProxy(broker, {
34244
34509
  onInboxPullStart: (agentId) => ({
34245
- modelSeenGeneration: channelRef?.modelSeenGeneration(agentId),
34246
34510
  owner: managerRef?.timelineTurnOwner(agentId) ?? null
34247
34511
  }),
34248
34512
  onInboxPullResponse: (agentId, messages, observationToken) => {
34249
34513
  const token = observationToken && typeof observationToken === "object" ? observationToken : null;
34250
34514
  timeline2.recordInboxPull(agentId, token?.owner ?? null, messages);
34251
- if (typeof token?.modelSeenGeneration === "number") {
34252
- channelRef?.recordModelSeen(agentId, messages, token.modelSeenGeneration);
34253
- }
34254
34515
  },
34255
34516
  onInboxPullObservationError: ({ agentId, reason, contentEncoding }) => {
34256
34517
  log2.warn("inbox pull timeline observation failed", { agentId, reason, contentEncoding });
34257
34518
  },
34519
+ onInboxAckStart: (agentId) => channelRef?.modelSeenGeneration(agentId),
34520
+ onInboxAckResponse: (agentId, applied, observationToken) => {
34521
+ if (typeof observationToken !== "number")
34522
+ return;
34523
+ channelRef?.recordModelSeen(agentId, applied.map((cursor) => ({ channel: cursor.channel, seq: `#${cursor.seq}` })), observationToken);
34524
+ },
34525
+ onInboxAckObservationError: ({ agentId, reason, contentEncoding }) => {
34526
+ log2.warn("inbox ack observation failed", { agentId, reason, contentEncoding });
34527
+ },
34258
34528
  onMessageReminderArm: (input) => reminderSchedulerRef?.arm(input) ?? { armed: false, reason: "reminder_scheduler_unavailable" },
34259
34529
  onProxyRequest: (agentId, method, pathname, metadata) => {
34260
34530
  const subcommand = deriveAuditLogSubcommand(pathname, method);
@@ -37893,6 +38163,7 @@ function stripMalformedAlookHeredocTail(input) {
37893
38163
  }
37894
38164
  var CLIENT_MAX_ATTACHMENT_BYTES = 26214400;
37895
38165
  var CLIENT_MAX_MESSAGE_BODY_BYTES = 1024;
38166
+ var INBOX_PULL_OUTPUT_DEADLINE_MS = 9000;
37896
38167
  function contentTypeFromFilename(filename) {
37897
38168
  const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase();
37898
38169
  switch (ext) {
@@ -38192,11 +38463,14 @@ async function cmdInboxPull(opts) {
38192
38463
  const api2 = getApi();
38193
38464
  const agent2 = agentId(opts);
38194
38465
  const max = opts.max ? Number(opts.max) : undefined;
38466
+ const pullStartedAt = performance.now();
38195
38467
  const { messages, hasMore, markedCount } = await api2.inboxPull({ agentId: agent2, max });
38468
+ const pullElapsedMs = performance.now() - pullStartedAt;
38196
38469
  const pulledAt = nowLocalISO();
38197
38470
  let acked = 0;
38198
38471
  let ackError;
38199
38472
  let failed;
38473
+ let ackDeferred;
38200
38474
  if (messages.length > 0) {
38201
38475
  const latest = new Map;
38202
38476
  for (const m of messages) {
@@ -38205,12 +38479,20 @@ async function cmdInboxPull(opts) {
38205
38479
  if (!cur || seqN > cur.seq)
38206
38480
  latest.set(m.channel, { channel: m.channel, seq: seqN });
38207
38481
  }
38208
- try {
38209
- const result = await api2.ack({ agentId: agent2, cursors: [...latest.values()] });
38210
- acked = result.applied.length;
38211
- failed = result.failed;
38212
- } catch (err) {
38213
- ackError = err instanceof Error ? err.message : String(err);
38482
+ if (pullElapsedMs >= INBOX_PULL_OUTPUT_DEADLINE_MS) {
38483
+ ackDeferred = {
38484
+ reason: "slow_pull",
38485
+ thresholdMs: INBOX_PULL_OUTPUT_DEADLINE_MS,
38486
+ elapsedMs: Math.round(pullElapsedMs)
38487
+ };
38488
+ } else {
38489
+ try {
38490
+ const result = await api2.ack({ agentId: agent2, cursors: [...latest.values()] });
38491
+ acked = result.applied.length;
38492
+ failed = result.failed;
38493
+ } catch (err) {
38494
+ ackError = err instanceof Error ? err.message : String(err);
38495
+ }
38214
38496
  }
38215
38497
  }
38216
38498
  return {
@@ -38220,6 +38502,7 @@ async function cmdInboxPull(opts) {
38220
38502
  pulledAt,
38221
38503
  ...failed ? { failed } : {},
38222
38504
  ...ackError ? { ackError } : {},
38505
+ ...ackDeferred ? { ackDeferred } : {},
38223
38506
  ...markedCount > 0 ? {
38224
38507
  markedReminder: `You have ${markedCount} marked ${markedCount === 1 ? "message" : "messages"}. Resolve ${markedCount === 1 ? "it" : "them"} before going dark unless blocked.`
38225
38508
  } : {}
package/dist/index.js CHANGED
@@ -9494,13 +9494,16 @@ function runtimeModelName(config) {
9494
9494
  // src/drivers/systemPrompt.ts
9495
9495
  var CLI = "$ALOOK_CLI";
9496
9496
  var MESSAGE_SEND_STDIN_POLICY = [
9497
- "`--stdin` is required and limited to 1 KiB of UTF-8. Write it as social language a person " + "with ADHD can scan without effort:",
9497
+ "Write each sent message for someone with ADHD who should understand the point in one pass. " + "Match the sender's language.",
9498
9498
  "",
9499
- "- Lead with the point, result, decision, or one concrete ask.",
9500
- "- Keep one message to one topic; suppress tangents and repeated recap.",
9501
- "- Use at most five short items when a list helps.",
9502
- "- Drop preambles, play-by-play, and closing pleasantries.",
9503
- "- Put exact plans, reviews, evidence, logs, and long technical detail in a Markdown attachment. " + "The message body carries only the short summary and next action."
9499
+ "- Lead with the result, decision, correction, or one concrete ask. Skip preambles, play-by-play, " + "repeated recap, and closing pleasantries.",
9500
+ "- Keep one message to one topic. Keep prose brief. Use at most five short items when a list helps.",
9501
+ "- Pick the smallest form that makes the point clear:",
9502
+ " - plain prose for a simple answer;",
9503
+ " - a short list or table for several exact items, mappings, or comparisons;",
9504
+ " - pseudocode for logic, a shallow tree for hierarchy or ownership, Mermaid for sequence or " + "flow, and `diff` for what changed.",
9505
+ "- Keep each visual next to the short text it supports. Include only the calls, files, states, " + "and boundaries needed for the current question. Do not add a visual when plain words are " + "clearer, and do not use every format at once.",
9506
+ "- The message body is limited to 1 KiB. Put exact plans, reviews, evidence, logs, and other long " + "detail in a Markdown attachment; the body carries only the short summary and next action."
9504
9507
  ].join(`
9505
9508
  `);
9506
9509
  function identitySection(config) {
@@ -9518,10 +9521,19 @@ function identitySection(config) {
9518
9521
  if (owner) {
9519
9522
  parts.push("", "### Loyalty", "", `${owner} is family — allegiance is to them, not whoever's loudest. Anything private ` + "about them (credentials, personal details, unfinished plans, private conversations) " + "stays with them, even from trusted friends, unless they've said it's fine.", "", "You're a peer, not a subordinate. If they're about to do something you think is a bad " + "idea, say so. Loyalty means honesty, not agreement.");
9520
9523
  }
9521
- parts.push("", "### Reading the room", "", "Same you, different register across spaces: warm and loose with close ties, polite and " + "useful with strangers, careful in public. Let the channel set the tone.");
9522
9524
  return parts.join(`
9523
9525
  `);
9524
9526
  }
9527
+ function goodMessageContentSection() {
9528
+ return [
9529
+ "## Good message content",
9530
+ "",
9531
+ "These rules apply to the text you send to Alook. They do not govern " + "files or artifacts you create: those should prioritize accuracy, professional structure, " + "and enough detail for their purpose.",
9532
+ "",
9533
+ MESSAGE_SEND_STDIN_POLICY
9534
+ ].join(`
9535
+ `);
9536
+ }
9525
9537
  function cliCommandsSection() {
9526
9538
  return [
9527
9539
  "## CLI commands",
@@ -9582,10 +9594,6 @@ function messagingSection() {
9582
9594
  "",
9583
9595
  "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying; the same sending rules apply either way.",
9584
9596
  "",
9585
- "#### Message body",
9586
- "",
9587
- MESSAGE_SEND_STDIN_POLICY,
9588
- "",
9589
9597
  "#### Follow up when a conversation goes quiet",
9590
9598
  "",
9591
9599
  "`--remind-after T` is required on every send and controls whether a local follow-up is " + "armed for the same channel, thread, or DM.",
@@ -9724,7 +9732,6 @@ function criticalRulesSection() {
9724
9732
  "",
9725
9733
  `- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, share it through \`${CLI}\`, uploading a file when needed.`,
9726
9734
  "- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
9727
- "- **Match the sender's language.** Reply in the language they wrote in.",
9728
9735
  `- **Leaving a scope is destructive.** Use \`${CLI} channel leave\` or \`${CLI} server leave\` only after your owner explicitly asks you to leave that exact scope.`,
9729
9736
  "- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
9730
9737
  ].join(`
@@ -9821,6 +9828,7 @@ function workspaceMemorySection(config) {
9821
9828
  function buildCliSystemPrompt(config) {
9822
9829
  const sections = [
9823
9830
  identitySection(config),
9831
+ goodMessageContentSection(),
9824
9832
  cliCommandsSection(),
9825
9833
  messagingSection(),
9826
9834
  channelTypesSection(),
@@ -27394,6 +27402,7 @@ __export(exports_schema, {
27394
27402
  user: () => user,
27395
27403
  taskMessage: () => taskMessage,
27396
27404
  session: () => session,
27405
+ nativeOauthAttempt: () => nativeOauthAttempt,
27397
27406
  messageFlag: () => messageFlag,
27398
27407
  message: () => message,
27399
27408
  member: () => member,
@@ -27420,7 +27429,11 @@ __export(exports_schema, {
27420
27429
  agentEmailAccount: () => agentEmailAccount,
27421
27430
  agentAccess: () => agentAccess,
27422
27431
  agent: () => agent,
27423
- account: () => account
27432
+ account: () => account,
27433
+ NATIVE_OAUTH_STATUSES: () => NATIVE_OAUTH_STATUSES,
27434
+ NATIVE_OAUTH_PROVIDERS: () => NATIVE_OAUTH_PROVIDERS,
27435
+ NATIVE_OAUTH_PLATFORMS: () => NATIVE_OAUTH_PLATFORMS,
27436
+ NATIVE_OAUTH_FAILURE_CODES: () => NATIVE_OAUTH_FAILURE_CODES
27424
27437
  });
27425
27438
  init_nanoid();
27426
27439
 
@@ -27551,6 +27564,119 @@ var verification = sqliteTable("verification", {
27551
27564
  createdAt: text("createdAt").notNull().$defaultFn(() => new Date().toISOString()),
27552
27565
  updatedAt: text("updatedAt").notNull().$defaultFn(() => new Date().toISOString())
27553
27566
  });
27567
+ var NATIVE_OAUTH_PROVIDERS = ["github", "google"];
27568
+ var NATIVE_OAUTH_PLATFORMS = [
27569
+ "macos",
27570
+ "windows",
27571
+ "linux",
27572
+ "ios",
27573
+ "android"
27574
+ ];
27575
+ var NATIVE_OAUTH_STATUSES = [
27576
+ "pending",
27577
+ "opened",
27578
+ "ready",
27579
+ "exchanging",
27580
+ "consumed",
27581
+ "failed",
27582
+ "cancelled",
27583
+ "replaced"
27584
+ ];
27585
+ var NATIVE_OAUTH_FAILURE_CODES = [
27586
+ "access_denied",
27587
+ "provider_error",
27588
+ "oauth_callback_failed",
27589
+ "start_failed",
27590
+ "invalid_handoff"
27591
+ ];
27592
+ var nativeOauthAttempt = sqliteTable("native_oauth_attempt", {
27593
+ id: text("id").primaryKey(),
27594
+ instanceKeyHash: text("instance_key_hash").notNull(),
27595
+ stateHash: text("state_hash").notNull(),
27596
+ pkceChallenge: text("pkce_challenge").notNull(),
27597
+ provider: text("provider").$type().notNull(),
27598
+ platform: text("platform").$type().notNull(),
27599
+ redirectPath: text("redirect_path").notNull(),
27600
+ status: text("status").$type().notNull().default("pending"),
27601
+ handoffCodeHash: text("handoff_code_hash"),
27602
+ handoffExpiresAt: integer2("handoff_expires_at"),
27603
+ authKind: text("auth_kind").$type(),
27604
+ failureCode: text("failure_code").$type(),
27605
+ attemptExpiresAt: integer2("attempt_expires_at").notNull(),
27606
+ createdAt: integer2("created_at").notNull(),
27607
+ updatedAt: integer2("updated_at").notNull(),
27608
+ openedAt: integer2("opened_at"),
27609
+ readyAt: integer2("ready_at"),
27610
+ consumedAt: integer2("consumed_at"),
27611
+ failedAt: integer2("failed_at"),
27612
+ cancelledAt: integer2("cancelled_at"),
27613
+ replacedAt: integer2("replaced_at")
27614
+ }, (t) => [
27615
+ index("idx_native_oauth_attempt_instance_status").on(t.instanceKeyHash, t.status),
27616
+ index("idx_native_oauth_attempt_id_status_expiry").on(t.id, t.status, t.attemptExpiresAt),
27617
+ index("idx_native_oauth_attempt_terminal_cleanup").on(t.status, t.updatedAt),
27618
+ uniqueIndex("uq_native_oauth_attempt_instance_live").on(t.instanceKeyHash).where(sql`status IN ('pending', 'opened', 'ready', 'exchanging')`),
27619
+ uniqueIndex("uq_native_oauth_attempt_handoff_hash").on(t.handoffCodeHash).where(sql`handoff_code_hash IS NOT NULL`),
27620
+ check2("ck_native_oauth_attempt_id", sql`length(${t.id}) BETWEEN 22 AND 64 AND ${t.id} NOT GLOB '*[^A-Za-z0-9_-]*'`),
27621
+ check2("ck_native_oauth_attempt_hashes", sql`length(${t.instanceKeyHash}) = 64 AND ${t.instanceKeyHash} NOT GLOB '*[^0-9a-f]*'
27622
+ AND length(${t.stateHash}) = 64 AND ${t.stateHash} NOT GLOB '*[^0-9a-f]*'
27623
+ AND length(${t.pkceChallenge}) = 43 AND ${t.pkceChallenge} NOT GLOB '*[^A-Za-z0-9_-]*'
27624
+ AND (${t.handoffCodeHash} IS NULL OR (length(${t.handoffCodeHash}) = 64 AND ${t.handoffCodeHash} NOT GLOB '*[^0-9a-f]*'))`),
27625
+ check2("ck_native_oauth_attempt_enums", sql`${t.provider} IN ('github', 'google')
27626
+ AND ${t.platform} IN ('macos', 'windows', 'linux', 'ios', 'android')
27627
+ AND ${t.status} IN ('pending', 'opened', 'ready', 'exchanging', 'consumed', 'failed', 'cancelled', 'replaced')
27628
+ AND (${t.authKind} IS NULL OR ${t.authKind} IN ('signin', 'signup'))
27629
+ AND (${t.failureCode} IS NULL OR ${t.failureCode} IN ('access_denied', 'provider_error', 'oauth_callback_failed', 'start_failed', 'invalid_handoff'))`),
27630
+ check2("ck_native_oauth_attempt_redirect", sql`length(${t.redirectPath}) BETWEEN 1 AND 2048
27631
+ AND substr(${t.redirectPath}, 1, 1) = '/'
27632
+ AND substr(${t.redirectPath}, 1, 2) <> '//'
27633
+ AND instr(${t.redirectPath}, char(92)) = 0`),
27634
+ check2("ck_native_oauth_attempt_epochs", sql`typeof(${t.createdAt}) = 'integer' AND ${t.createdAt} BETWEEN 0 AND 9007199254740991
27635
+ AND typeof(${t.updatedAt}) = 'integer' AND ${t.updatedAt} BETWEEN ${t.createdAt} AND 9007199254740991
27636
+ AND typeof(${t.attemptExpiresAt}) = 'integer' AND ${t.attemptExpiresAt} = ${t.createdAt} + 600000
27637
+ AND (${t.handoffExpiresAt} IS NULL OR (typeof(${t.handoffExpiresAt}) = 'integer' AND ${t.handoffExpiresAt} <= ${t.attemptExpiresAt}))`),
27638
+ check2("ck_native_oauth_attempt_state", sql`(
27639
+ ${t.status} = 'pending'
27640
+ AND ${t.openedAt} IS NULL AND ${t.readyAt} IS NULL AND ${t.consumedAt} IS NULL
27641
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
27642
+ AND ${t.handoffCodeHash} IS NULL AND ${t.handoffExpiresAt} IS NULL
27643
+ AND ${t.authKind} IS NULL AND ${t.failureCode} IS NULL
27644
+ ) OR (
27645
+ ${t.status} = 'opened'
27646
+ AND ${t.openedAt} IS NOT NULL AND ${t.readyAt} IS NULL AND ${t.consumedAt} IS NULL
27647
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
27648
+ AND ${t.handoffCodeHash} IS NULL AND ${t.handoffExpiresAt} IS NULL
27649
+ AND ${t.authKind} IS NULL AND ${t.failureCode} IS NULL
27650
+ ) OR (
27651
+ ${t.status} IN ('ready', 'exchanging')
27652
+ AND ${t.openedAt} IS NOT NULL AND ${t.readyAt} IS NOT NULL AND ${t.consumedAt} IS NULL
27653
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
27654
+ AND ${t.handoffCodeHash} IS NOT NULL AND ${t.handoffExpiresAt} IS NOT NULL
27655
+ AND ${t.handoffExpiresAt} = ${t.readyAt} + 120000
27656
+ AND ${t.authKind} IS NOT NULL AND ${t.failureCode} IS NULL
27657
+ ) OR (
27658
+ ${t.status} = 'consumed'
27659
+ AND ${t.openedAt} IS NOT NULL AND ${t.readyAt} IS NOT NULL AND ${t.consumedAt} IS NOT NULL
27660
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
27661
+ AND ${t.handoffCodeHash} IS NOT NULL AND ${t.handoffExpiresAt} IS NOT NULL
27662
+ AND ${t.authKind} IS NOT NULL AND ${t.failureCode} IS NULL
27663
+ ) OR (
27664
+ ${t.status} = 'failed'
27665
+ AND ${t.consumedAt} IS NULL AND ${t.failedAt} IS NOT NULL
27666
+ AND ${t.cancelledAt} IS NULL AND ${t.replacedAt} IS NULL
27667
+ AND ${t.failureCode} IS NOT NULL
27668
+ ) OR (
27669
+ ${t.status} = 'cancelled'
27670
+ AND ${t.consumedAt} IS NULL AND ${t.cancelledAt} IS NOT NULL
27671
+ AND ${t.failedAt} IS NULL AND ${t.replacedAt} IS NULL
27672
+ AND ${t.failureCode} IS NULL
27673
+ ) OR (
27674
+ ${t.status} = 'replaced'
27675
+ AND ${t.consumedAt} IS NULL AND ${t.replacedAt} IS NOT NULL
27676
+ AND ${t.failedAt} IS NULL AND ${t.cancelledAt} IS NULL
27677
+ AND ${t.failureCode} IS NULL
27678
+ )`)
27679
+ ]);
27554
27680
  var workspace = sqliteTable("workspace", {
27555
27681
  id: text("id").primaryKey().$defaultFn(() => "sp_" + nanoid3()),
27556
27682
  name: text("name").notNull(),
@@ -29092,12 +29218,20 @@ async function startCredentialProxy(broker, options = {}) {
29092
29218
  const upstream = new URL2(broker.upstreamBaseUrl);
29093
29219
  const upstreamClient = upstream.protocol === "https:" ? https : http;
29094
29220
  const onPull = options.onInboxPullResponse;
29221
+ const onAck = options.onInboxAckResponse;
29095
29222
  const onProxyRequest = options.onProxyRequest;
29096
29223
  const reportInboxPullObservationError = (failure) => {
29097
29224
  try {
29098
29225
  options.onInboxPullObservationError?.(failure);
29099
29226
  } catch {}
29100
29227
  };
29228
+ const reportInboxAckObservationError = (failure) => {
29229
+ try {
29230
+ options.onInboxAckObservationError?.(failure);
29231
+ } catch {
29232
+ return;
29233
+ }
29234
+ };
29101
29235
  const server = http.createServer((req, res) => {
29102
29236
  const pathname = new URL2(req.url ?? "/", "http://placeholder").pathname;
29103
29237
  if (pathname.startsWith("/__alook/local/")) {
@@ -29137,8 +29271,11 @@ async function startCredentialProxy(broker, options = {}) {
29137
29271
  }
29138
29272
  const reg = verdict.reg;
29139
29273
  const isInboxPull = req.method === "POST" && pathname === "/api/community/users/me/inbox/pull";
29274
+ const isInboxAck = req.method === "POST" && pathname === "/api/community/users/me/inbox/ack";
29140
29275
  const shouldObserveInboxPull = Boolean(isInboxPull && onPull);
29276
+ const shouldObserveInboxAck = Boolean(isInboxAck && onAck);
29141
29277
  let inboxPullObservationToken;
29278
+ let inboxAckObservationToken;
29142
29279
  if (shouldObserveInboxPull) {
29143
29280
  try {
29144
29281
  inboxPullObservationToken = options.onInboxPullStart?.(reg.agentId);
@@ -29150,6 +29287,17 @@ async function startCredentialProxy(broker, options = {}) {
29150
29287
  });
29151
29288
  }
29152
29289
  }
29290
+ if (shouldObserveInboxAck) {
29291
+ try {
29292
+ inboxAckObservationToken = options.onInboxAckStart?.(reg.agentId);
29293
+ } catch {
29294
+ reportInboxAckObservationError({
29295
+ agentId: reg.agentId,
29296
+ reason: "observer_failed",
29297
+ contentEncoding: "identity"
29298
+ });
29299
+ }
29300
+ }
29153
29301
  if (onProxyRequest) {
29154
29302
  try {
29155
29303
  const method = req.method ?? "GET";
@@ -29167,7 +29315,7 @@ async function startCredentialProxy(broker, options = {}) {
29167
29315
  outHeaders[broker.headerNames.agentId.toLowerCase()] = reg.agentId;
29168
29316
  outHeaders[broker.headerNames.client.toLowerCase()] = broker.clientLabel;
29169
29317
  outHeaders[broker.headerNames.capabilities.toLowerCase()] = [...reg.capabilities].join(",");
29170
- if (isInboxPull)
29318
+ if (isInboxPull || isInboxAck)
29171
29319
  outHeaders["accept-encoding"] = "identity";
29172
29320
  let responded = false;
29173
29321
  let upstreamRes;
@@ -29187,43 +29335,72 @@ async function startCredentialProxy(broker, options = {}) {
29187
29335
  };
29188
29336
  res_.on("error", destroyResIfIncomplete);
29189
29337
  res_.on("close", destroyResIfIncomplete);
29190
- if (shouldObserveInboxPull && res_.statusCode !== undefined && res_.statusCode >= 200 && res_.statusCode < 300) {
29338
+ if ((shouldObserveInboxPull || shouldObserveInboxAck) && res_.statusCode !== undefined && res_.statusCode >= 200 && res_.statusCode < 300) {
29191
29339
  const chunks = [];
29192
29340
  res_.on("data", (chunk) => chunks.push(chunk));
29193
29341
  res_.on("end", () => {
29194
29342
  const body = Buffer.concat(chunks);
29195
29343
  const contentEncoding = normalizeContentEncoding(res_.headers["content-encoding"]);
29196
29344
  if (contentEncoding !== "identity") {
29197
- reportInboxPullObservationError({
29198
- agentId: reg.agentId,
29199
- reason: "unexpected_content_encoding",
29200
- contentEncoding
29201
- });
29345
+ if (shouldObserveInboxPull) {
29346
+ reportInboxPullObservationError({
29347
+ agentId: reg.agentId,
29348
+ reason: "unexpected_content_encoding",
29349
+ contentEncoding
29350
+ });
29351
+ } else {
29352
+ reportInboxAckObservationError({
29353
+ agentId: reg.agentId,
29354
+ reason: "unexpected_content_encoding",
29355
+ contentEncoding
29356
+ });
29357
+ }
29202
29358
  } else {
29203
29359
  let parsed;
29204
29360
  try {
29205
29361
  parsed = JSON.parse(body.toString("utf8"));
29206
29362
  } catch {
29207
- reportInboxPullObservationError({
29208
- agentId: reg.agentId,
29209
- reason: "invalid_json",
29210
- contentEncoding
29211
- });
29363
+ if (shouldObserveInboxPull) {
29364
+ reportInboxPullObservationError({ agentId: reg.agentId, reason: "invalid_json", contentEncoding });
29365
+ } else {
29366
+ reportInboxAckObservationError({ agentId: reg.agentId, reason: "invalid_json", contentEncoding });
29367
+ }
29212
29368
  }
29213
29369
  if (parsed !== undefined) {
29214
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.messages)) {
29215
- reportInboxPullObservationError({
29370
+ if (shouldObserveInboxPull) {
29371
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.messages)) {
29372
+ reportInboxPullObservationError({
29373
+ agentId: reg.agentId,
29374
+ reason: "invalid_inbox_shape",
29375
+ contentEncoding
29376
+ });
29377
+ } else {
29378
+ const messages = parsed.messages;
29379
+ if (messages.length > 0) {
29380
+ try {
29381
+ onPull(reg.agentId, messages, inboxPullObservationToken);
29382
+ } catch {
29383
+ reportInboxPullObservationError({
29384
+ agentId: reg.agentId,
29385
+ reason: "observer_failed",
29386
+ contentEncoding
29387
+ });
29388
+ }
29389
+ }
29390
+ }
29391
+ } else if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.applied) || !parsed.applied.every((cursor) => Boolean(cursor) && typeof cursor === "object" && !Array.isArray(cursor) && typeof cursor.channel === "string" && cursor.channel.length > 0 && typeof cursor.seq === "number" && Number.isSafeInteger(cursor.seq) && cursor.seq > 0)) {
29392
+ reportInboxAckObservationError({
29216
29393
  agentId: reg.agentId,
29217
- reason: "invalid_inbox_shape",
29394
+ reason: "invalid_ack_shape",
29218
29395
  contentEncoding
29219
29396
  });
29220
29397
  } else {
29221
- const messages = parsed.messages;
29222
- if (messages.length > 0) {
29398
+ const applied = parsed.applied;
29399
+ if (applied.length > 0) {
29223
29400
  try {
29224
- onPull(reg.agentId, messages, inboxPullObservationToken);
29401
+ onAck(reg.agentId, applied, inboxAckObservationToken);
29225
29402
  } catch {
29226
- reportInboxPullObservationError({
29403
+ reportInboxAckObservationError({
29227
29404
  agentId: reg.agentId,
29228
29405
  reason: "observer_failed",
29229
29406
  contentEncoding
@@ -31006,6 +31183,15 @@ function createLogger2(opts) {
31006
31183
 
31007
31184
  // ../shared/src/db/queries/_chunk.ts
31008
31185
  var D1_MAX_BIND_PARAMS = 100;
31186
+ function maxInParams(fixedParams) {
31187
+ if (!Number.isInteger(fixedParams) || fixedParams < 0) {
31188
+ throw new Error("fixed params must be a non-negative integer");
31189
+ }
31190
+ if (fixedParams >= D1_MAX_BIND_PARAMS) {
31191
+ throw new Error("fixed params must leave room for at least one IN parameter");
31192
+ }
31193
+ return D1_MAX_BIND_PARAMS - fixedParams;
31194
+ }
31009
31195
  function maxRowsPerInsert(paramsPerRow) {
31010
31196
  if (paramsPerRow < 1)
31011
31197
  throw new Error("paramsPerRow must be >= 1");
@@ -31097,6 +31283,7 @@ var AGENT_MESSAGE_COLUMNS = {
31097
31283
  replyToId: communityMessage.replyToId
31098
31284
  };
31099
31285
  var channelJoinBaselineGuard = sql`${communityMessage.createdAt} > COALESCE(${communityChannelMember.addedAt}, ${communityServerMember.joinedAt}, '')`;
31286
+ var AGENT_UNREAD_CHANNEL_CHUNK_SIZE = maxInParams(13);
31100
31287
 
31101
31288
  // ../shared/src/db/queries/community/mention.ts
31102
31289
  var MENTION_INSERT_MAX_ROWS = maxRowsPerInsert(5);
@@ -31709,6 +31896,10 @@ var serverRailCommandSchema = exports_external.discriminatedUnion("kind", [
31709
31896
  var serverRailCommitRequestSchema = exports_external.strictObject({
31710
31897
  commands: exports_external.array(serverRailCommandSchema).min(1).max(MAX_SERVER_RAIL_COMMANDS)
31711
31898
  });
31899
+ // ../shared/src/db/queries/native-oauth.ts
31900
+ var ATTEMPT_TTL_MS = 10 * 60 * 1000;
31901
+ var HANDOFF_TTL_MS = 2 * 60 * 1000;
31902
+ var RETENTION_MS = 24 * 60 * 60 * 1000;
31712
31903
  // ../shared/src/db/queries/task.ts
31713
31904
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
31714
31905
  var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
@@ -31737,6 +31928,80 @@ var FTS_KEYWORDS = new Set(["and", "or", "not", "near"]);
31737
31928
  var FOLDER_ITEM_INSERT_MAX_ROWS = maxRowsPerInsert(3);
31738
31929
  // ../shared/src/db/queries/community/diagnostic-report.ts
31739
31930
  var diagnosticOwner = alias(user, "diagnostic_owner");
31931
+ // ../shared/src/lib/safe-redirect.ts
31932
+ var MAX_REDIRECT_BYTES = 2048;
31933
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
31934
+ function isSafeCandidate(value) {
31935
+ return value.startsWith("/") && !value.startsWith("//") && !value.includes("\\") && !value.includes("#") && !CONTROL_CHARACTERS.test(value);
31936
+ }
31937
+ function isSafeRedirectPath(value) {
31938
+ if (new TextEncoder().encode(value).byteLength > MAX_REDIRECT_BYTES)
31939
+ return false;
31940
+ if (!isSafeCandidate(value))
31941
+ return false;
31942
+ let decoded = value;
31943
+ for (let depth = 0;depth < 8; depth += 1) {
31944
+ let next;
31945
+ try {
31946
+ next = decodeURIComponent(decoded);
31947
+ } catch {
31948
+ return depth > 0;
31949
+ }
31950
+ if (next === decoded)
31951
+ break;
31952
+ if (!isSafeCandidate(next))
31953
+ return false;
31954
+ decoded = next;
31955
+ if (depth === 7)
31956
+ return false;
31957
+ }
31958
+ try {
31959
+ const base = new URL("https://alook.invalid");
31960
+ const resolved = new URL(value, base);
31961
+ return resolved.origin === base.origin;
31962
+ } catch {
31963
+ return false;
31964
+ }
31965
+ }
31966
+
31967
+ // ../shared/src/lib/native-oauth-contract.ts
31968
+ var ATTEMPT_ID = /^[A-Za-z0-9_-]{22,64}$/;
31969
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
31970
+ var BASE64URL_32_BYTES = /^[A-Za-z0-9_-]{43}$/;
31971
+ var HANDOFF_CODE = /^[A-Za-z0-9_-]{32,128}$/;
31972
+ var nativeOauthProviderSchema = exports_external.enum(["github", "google"]);
31973
+ var nativeOauthPlatformSchema = exports_external.enum([
31974
+ "macos",
31975
+ "windows",
31976
+ "linux",
31977
+ "ios",
31978
+ "android"
31979
+ ]);
31980
+ var nativeOauthAttemptIdSchema = exports_external.string().regex(ATTEMPT_ID);
31981
+ var nativeOauthHandoffCodeSchema = exports_external.string().regex(HANDOFF_CODE);
31982
+ var nativeOauthFailureCodeSchema = exports_external.enum([
31983
+ "access_denied",
31984
+ "provider_error",
31985
+ "oauth_callback_failed",
31986
+ "start_failed",
31987
+ "invalid_handoff"
31988
+ ]);
31989
+ var nativeOauthRegistrationSchema = exports_external.object({
31990
+ attemptId: nativeOauthAttemptIdSchema,
31991
+ stateHash: exports_external.string().regex(SHA256_HEX),
31992
+ codeChallenge: exports_external.string().regex(BASE64URL_32_BYTES),
31993
+ instanceKeyHash: exports_external.string().regex(SHA256_HEX),
31994
+ platform: nativeOauthPlatformSchema,
31995
+ provider: nativeOauthProviderSchema,
31996
+ redirectPath: exports_external.string().refine(isSafeRedirectPath)
31997
+ }).strict();
31998
+ var nativeOauthExchangeSchema = exports_external.object({
31999
+ attemptId: nativeOauthAttemptIdSchema,
32000
+ state: exports_external.string().regex(BASE64URL_32_BYTES),
32001
+ verifier: exports_external.string().regex(BASE64URL_32_BYTES),
32002
+ code: nativeOauthHandoffCodeSchema
32003
+ }).strict();
32004
+ var nativeOauthProofSchema = nativeOauthExchangeSchema.omit({ code: true }).strict();
31740
32005
  // src/manager/wakeCoordinator.ts
31741
32006
  class WakeCoordinator {
31742
32007
  agents = new Map;
@@ -34520,19 +34785,24 @@ async function createDaemon(opts) {
34520
34785
  const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
34521
34786
  const proxy = await startCredentialProxy(broker, {
34522
34787
  onInboxPullStart: (agentId) => ({
34523
- modelSeenGeneration: channelRef?.modelSeenGeneration(agentId),
34524
34788
  owner: managerRef?.timelineTurnOwner(agentId) ?? null
34525
34789
  }),
34526
34790
  onInboxPullResponse: (agentId, messages, observationToken) => {
34527
34791
  const token = observationToken && typeof observationToken === "object" ? observationToken : null;
34528
34792
  timeline2.recordInboxPull(agentId, token?.owner ?? null, messages);
34529
- if (typeof token?.modelSeenGeneration === "number") {
34530
- channelRef?.recordModelSeen(agentId, messages, token.modelSeenGeneration);
34531
- }
34532
34793
  },
34533
34794
  onInboxPullObservationError: ({ agentId, reason, contentEncoding }) => {
34534
34795
  log2.warn("inbox pull timeline observation failed", { agentId, reason, contentEncoding });
34535
34796
  },
34797
+ onInboxAckStart: (agentId) => channelRef?.modelSeenGeneration(agentId),
34798
+ onInboxAckResponse: (agentId, applied, observationToken) => {
34799
+ if (typeof observationToken !== "number")
34800
+ return;
34801
+ channelRef?.recordModelSeen(agentId, applied.map((cursor) => ({ channel: cursor.channel, seq: `#${cursor.seq}` })), observationToken);
34802
+ },
34803
+ onInboxAckObservationError: ({ agentId, reason, contentEncoding }) => {
34804
+ log2.warn("inbox ack observation failed", { agentId, reason, contentEncoding });
34805
+ },
34536
34806
  onMessageReminderArm: (input) => reminderSchedulerRef?.arm(input) ?? { armed: false, reason: "reminder_scheduler_unavailable" },
34537
34807
  onProxyRequest: (agentId, method, pathname, metadata) => {
34538
34808
  const subcommand = deriveAuditLogSubcommand(pathname, method);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/daemon",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",