@primusclaw/personal-agent-runtime 0.1.1 → 0.3.0

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/index.js CHANGED
@@ -529,6 +529,12 @@ var Watchdog = class {
529
529
  };
530
530
 
531
531
  // packages/runtime/src/claw/remote-agents.ts
532
+ function enabledOf(answer2) {
533
+ const data = anyDataOf(answer2, "the approvals setting");
534
+ const enabled = data !== null && typeof data === "object" ? data["enabled"] : void 0;
535
+ if (typeof enabled !== "boolean") throw new ClawAnswerInvalid4("the approvals setting has no enabled");
536
+ return enabled;
537
+ }
532
538
  var HttpRemoteAgents = class {
533
539
  constructor(http, clientLabel) {
534
540
  this.http = http;
@@ -543,6 +549,12 @@ var HttpRemoteAgents = class {
543
549
  const path = options.includeHidden === true ? `/connectors?hidden=${REMOTE_HIDDEN_INCLUDE}` : "/connectors";
544
550
  return read(remoteConnectorsSchema, anyDataOf(await this.#call("GET", path, key2), "connectors"), "connectors");
545
551
  }
552
+ async approvals(key2) {
553
+ return enabledOf(await this.#call("GET", "/approvals", key2));
554
+ }
555
+ async setApprovals(key2, enabled) {
556
+ return enabledOf(await this.#call("PUT", "/approvals", key2, { enabled }));
557
+ }
546
558
  async pair(key2) {
547
559
  return read(
548
560
  remotePairingSchema,
@@ -1233,6 +1245,7 @@ function createClawClient(options) {
1233
1245
  // packages/runtime/src/claw/identity-lookup.ts
1234
1246
  import {
1235
1247
  ClawUnreachable as ClawUnreachable3,
1248
+ isApiKey as isApiKey3,
1236
1249
  pyLower,
1237
1250
  pyStrip as pyStrip3,
1238
1251
  pyStrOf as pyStrOf3,
@@ -1240,19 +1253,33 @@ import {
1240
1253
  } from "@primusclaw/personal-agent";
1241
1254
  var HttpIdentityLookup = class {
1242
1255
  /** `safeApiUrl` is the platform API without a trailing slash (Settings.safeApiUrl). */
1243
- constructor(safeApiUrl, fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
1256
+ constructor(safeApiUrl, fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
1244
1257
  this.timeoutMs = timeoutMs;
1245
- this.http = new ClawHttp(safeApiUrl, fetchImpl);
1258
+ this.safe = new ClawHttp(safeApiUrl, fetchImpl);
1259
+ this.claw = options.clawApiUrl ? new ClawHttp(options.clawApiUrl, fetchImpl) : void 0;
1260
+ this.logger = options.logger;
1246
1261
  }
1247
1262
  timeoutMs;
1248
- http;
1263
+ safe;
1264
+ claw;
1265
+ logger;
1249
1266
  async ownerOfKey(key2) {
1250
- const what = "/users/self";
1267
+ if (isApiKey3(key2)) {
1268
+ const body2 = await this.ask(this.safe, "/users/self", key2, "api_key");
1269
+ return body2 === void 0 ? void 0 : ownerIn(body2);
1270
+ }
1271
+ if (this.claw === void 0) return void 0;
1272
+ const body = await this.ask(this.claw, "/v1/me", key2, "identity_token");
1273
+ return body === void 0 ? void 0 : ownerInMe(body);
1274
+ }
1275
+ /** The JSON answer of `GET what` asked with `key`; undefined on any failure. */
1276
+ async ask(http, what, key2, kind) {
1251
1277
  let text3;
1252
1278
  try {
1253
- const response = await this.http.send({ method: "GET", path: what, key: key2, timeoutMs: this.timeoutMs });
1279
+ const response = await http.send({ method: "GET", path: what, key: key2, timeoutMs: this.timeoutMs });
1254
1280
  if (response.status !== 200) {
1255
1281
  await response.body?.cancel();
1282
+ this.logger?.info("identity lookup: no owner", { route: what, status: response.status, credential: kind });
1256
1283
  return void 0;
1257
1284
  }
1258
1285
  text3 = await readText(response, what);
@@ -1260,13 +1287,11 @@ var HttpIdentityLookup = class {
1260
1287
  if (err5 instanceof ClawUnreachable3) return void 0;
1261
1288
  throw err5;
1262
1289
  }
1263
- let body;
1264
1290
  try {
1265
- body = JSON.parse(text3);
1291
+ return JSON.parse(text3);
1266
1292
  } catch {
1267
1293
  return void 0;
1268
1294
  }
1269
- return ownerIn(body);
1270
1295
  }
1271
1296
  };
1272
1297
  function field(user, name) {
@@ -1285,6 +1310,10 @@ function ownerIn(body) {
1285
1310
  accountType: pyLower(field(user, "type"))
1286
1311
  };
1287
1312
  }
1313
+ function ownerInMe(body) {
1314
+ if (!isObject5(body)) return void 0;
1315
+ return { id: field(body, "userId"), name: field(body, "name"), email: field(body, "email"), accountType: "" };
1316
+ }
1288
1317
 
1289
1318
  // packages/runtime/src/claw/key-revoker.ts
1290
1319
  import { ClawUnreachable as ClawUnreachable4, pyTruthy as pyTruthy5 } from "@primusclaw/personal-agent";
@@ -1935,11 +1964,13 @@ import {
1935
1964
  DEFAULT_MAX_CONCURRENT_TASKS,
1936
1965
  DEFAULT_MESSAGE_LIMIT,
1937
1966
  DEFAULT_MESSAGE_WINDOW_SECONDS,
1967
+ DEFAULT_GROUP_CONTEXT,
1938
1968
  decimalBigInt as decimalBigInt2,
1939
1969
  parseLimits
1940
1970
  } from "@primusclaw/personal-agent";
1941
1971
  var GITHUB_CLI_CLIENT_ID = "178c6fc778ccc68e1d6a";
1942
1972
  var ASSISTANT_REPLY_WINDOW_SECONDS = 30 * 60;
1973
+ var MAX_GROUP_CONTEXT_MESSAGES = 200;
1943
1974
  function readSettings(env, secrets = /* @__PURE__ */ new Map()) {
1944
1975
  const read2 = new EnvReader(env);
1945
1976
  const store = { values: secrets, source: secretsSourceOf(env) };
@@ -1975,6 +2006,7 @@ function readSettings(env, secrets = /* @__PURE__ */ new Map()) {
1975
2006
  directChat: directChat(read2),
1976
2007
  onBehalfUsers: new Set(read2.list("TEAMS_ON_BEHALF_USERS")),
1977
2008
  observeUsers: new Set(read2.list("TEAMS_OBSERVE_USERS", true)),
2009
+ groupContext: groupContextSettings(read2),
1978
2010
  sensitiveLimits: limitsOf(read2),
1979
2011
  linkLimits: {
1980
2012
  perUser: read2.positiveInt("TEAMS_LINK_MAX_FAILURES_PER_USER", 5),
@@ -1989,6 +2021,19 @@ function readSettings(env, secrets = /* @__PURE__ */ new Map()) {
1989
2021
  secretsSource: store.source
1990
2022
  };
1991
2023
  }
2024
+ function groupContextSettings(read2) {
2025
+ const messages = read2.int("TEAMS_GROUP_CONTEXT_MESSAGES", String(DEFAULT_GROUP_CONTEXT.messages));
2026
+ if (messages < 0 || messages > MAX_GROUP_CONTEXT_MESSAGES) {
2027
+ throw new ConfigError2(
2028
+ `TEAMS_GROUP_CONTEXT_MESSAGES must be between 0 and ${String(MAX_GROUP_CONTEXT_MESSAGES)}, not ${String(messages)}`
2029
+ );
2030
+ }
2031
+ return {
2032
+ messages,
2033
+ maxAgeSeconds: read2.positiveInt("TEAMS_GROUP_CONTEXT_HOURS", DEFAULT_GROUP_CONTEXT.maxAgeSeconds / 3600) * 3600,
2034
+ messageChars: read2.positiveInt("TEAMS_GROUP_CONTEXT_CHARS", DEFAULT_GROUP_CONTEXT.messageChars)
2035
+ };
2036
+ }
1992
2037
  function secret(read2, store, name) {
1993
2038
  const loaded = store.values.get(name);
1994
2039
  if (loaded !== void 0) return loaded;
@@ -3315,6 +3360,7 @@ var SqliteIdempotencyRepository = class {
3315
3360
  };
3316
3361
 
3317
3362
  // packages/runtime/src/sqlite/link-repository.ts
3363
+ import { createHash as createHash2 } from "node:crypto";
3318
3364
  import {
3319
3365
  CODE_ALPHABET,
3320
3366
  CODE_LENGTH,
@@ -3333,8 +3379,12 @@ function forgetRepos(db, userId) {
3333
3379
  }
3334
3380
 
3335
3381
  // packages/runtime/src/sqlite/link-repository.ts
3382
+ function tokenHash(credential) {
3383
+ return createHash2("sha256").update(credential).digest("hex");
3384
+ }
3336
3385
  var PERSON_TABLES = [
3337
3386
  "links",
3387
+ "bound_users",
3338
3388
  "sessions",
3339
3389
  "conversation_refs",
3340
3390
  "held_events",
@@ -3433,6 +3483,7 @@ var SqliteLinkRepository = class {
3433
3483
  col.bytes(row, "key_enc"),
3434
3484
  now
3435
3485
  );
3486
+ db.run("DELETE FROM bound_users WHERE aad_id = ?", userId);
3436
3487
  forgetNotifySub(db, userId);
3437
3488
  forgetRepos(db, userId);
3438
3489
  db.run("DELETE FROM sessions WHERE aad_id = ?", userId);
@@ -3465,12 +3516,39 @@ var SqliteLinkRepository = class {
3465
3516
  setClawUserId(userId, clawUserId) {
3466
3517
  this.ctx.db.run("UPDATE links SET claw_user_id = ? WHERE aad_id = ?", clawUserId, userId);
3467
3518
  }
3519
+ boundClawUser(userId, credential) {
3520
+ const row = this.ctx.db.get("SELECT claw_user_id, token_hash FROM bound_users WHERE aad_id = ?", userId);
3521
+ if (row === void 0) return null;
3522
+ return { clawUserId: col.str(row, "claw_user_id"), current: col.str(row, "token_hash") === tokenHash(credential) };
3523
+ }
3524
+ /** Ignored for a linked person (a link's id is read for its key). Another account than before forgets the subscription. */
3525
+ setBoundClawUser(userId, clawUserId, credential) {
3526
+ const { db } = this.ctx;
3527
+ if (db.get("SELECT 1 AS one FROM links WHERE aad_id = ?", userId) !== void 0) return;
3528
+ const before = db.get("SELECT claw_user_id FROM bound_users WHERE aad_id = ?", userId);
3529
+ if (before !== void 0 && col.str(before, "claw_user_id") !== clawUserId) forgetNotifySub(db, userId);
3530
+ db.run(
3531
+ "INSERT OR REPLACE INTO bound_users (aad_id, claw_user_id, token_hash, updated_at) VALUES (?, ?, ?, ?)",
3532
+ userId,
3533
+ clawUserId,
3534
+ tokenHash(credential),
3535
+ this.ctx.now()
3536
+ );
3537
+ }
3538
+ /** Ignored for a linked person: their link keeps its own subscription. */
3539
+ forgetBound(userId) {
3540
+ const { db } = this.ctx;
3541
+ if (db.get("SELECT 1 AS one FROM links WHERE aad_id = ?", userId) !== void 0) return;
3542
+ if (db.run("DELETE FROM bound_users WHERE aad_id = ?", userId) > 0) forgetNotifySub(db, userId);
3543
+ }
3468
3544
  needsClawUserId(userId) {
3469
3545
  const row = this.ctx.db.get("SELECT claw_user_id FROM links WHERE aad_id = ?", userId);
3470
3546
  return row !== void 0 && !col.bool(row, "claw_user_id");
3471
3547
  }
3548
+ /** The link's (a linked person's id is never taken from anywhere else), else the bound person's. */
3472
3549
  clawUserIdOf(userId) {
3473
- const row = this.ctx.db.get("SELECT claw_user_id FROM links WHERE aad_id = ?", userId);
3550
+ const { db } = this.ctx;
3551
+ const row = db.get("SELECT claw_user_id FROM links WHERE aad_id = ?", userId) ?? db.get("SELECT claw_user_id FROM bound_users WHERE aad_id = ?", userId);
3474
3552
  return row && col.bool(row, "claw_user_id") ? col.str(row, "claw_user_id") : null;
3475
3553
  }
3476
3554
  linkedUsers() {
@@ -3608,6 +3686,15 @@ var ADDED_COLUMNS = [
3608
3686
  ]
3609
3687
  ],
3610
3688
  ["notify_subs", [["types_version", "INTEGER NOT NULL DEFAULT 0"]]],
3689
+ [
3690
+ "remote_permissions",
3691
+ [
3692
+ ["message_id", "TEXT NOT NULL DEFAULT ''"],
3693
+ // '' = the channel never said the card's id
3694
+ ["ask_json", "TEXT NOT NULL DEFAULT ''"]
3695
+ // '' = not a question a terminal asks
3696
+ ]
3697
+ ],
3611
3698
  [
3612
3699
  "turn_answers",
3613
3700
  [
@@ -3841,6 +3928,26 @@ var SqliteObserveRepository = class {
3841
3928
  }
3842
3929
  };
3843
3930
 
3931
+ // packages/runtime/src/sqlite/group-context-repository.ts
3932
+ var SqliteGroupContextRepository = class {
3933
+ constructor(ctx) {
3934
+ this.ctx = ctx;
3935
+ }
3936
+ ctx;
3937
+ contextOff(conversationId) {
3938
+ return this.ctx.db.get("SELECT 1 AS off FROM group_context_off WHERE conversation_id = ?", conversationId) !== void 0;
3939
+ }
3940
+ setContextOff(conversationId, off) {
3941
+ if (off) {
3942
+ this.ctx.db.run(
3943
+ "INSERT INTO group_context_off (conversation_id, updated_at) VALUES (?, ?) ON CONFLICT (conversation_id) DO UPDATE SET updated_at = excluded.updated_at",
3944
+ conversationId,
3945
+ this.ctx.now()
3946
+ );
3947
+ } else this.ctx.db.run("DELETE FROM group_context_off WHERE conversation_id = ?", conversationId);
3948
+ }
3949
+ };
3950
+
3844
3951
  // packages/runtime/src/sqlite/people-repository.ts
3845
3952
  import {
3846
3953
  DEFAULT_QUIET,
@@ -4044,7 +4151,7 @@ var SqliteRemoteSessionNameRepository = class {
4044
4151
  );
4045
4152
  }
4046
4153
  };
4047
- var PERMISSION_COLUMNS = "session_id, action_id, aad_id, tool, options_json, expires_at, state";
4154
+ var PERMISSION_COLUMNS = "session_id, action_id, aad_id, tool, options_json, expires_at, state, message_id, ask_json";
4048
4155
  function permission(row) {
4049
4156
  return {
4050
4157
  sessionId: col.str(row, "session_id"),
@@ -4053,9 +4160,20 @@ function permission(row) {
4053
4160
  tool: col.str(row, "tool"),
4054
4161
  options: jsonList(col.str(row, "options_json")),
4055
4162
  expiresAt: col.num(row, "expires_at"),
4056
- state: col.str(row, "state")
4163
+ state: col.str(row, "state"),
4164
+ messageId: col.str(row, "message_id"),
4165
+ ask: askOf(col.str(row, "ask_json"))
4057
4166
  };
4058
4167
  }
4168
+ function askOf(text3) {
4169
+ if (!text3) return null;
4170
+ try {
4171
+ const value = JSON.parse(text3);
4172
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
4173
+ } catch {
4174
+ return null;
4175
+ }
4176
+ }
4059
4177
  var SqliteRemotePermissionRepository = class {
4060
4178
  constructor(ctx) {
4061
4179
  this.ctx = ctx;
@@ -4065,14 +4183,15 @@ var SqliteRemotePermissionRepository = class {
4065
4183
  const now = this.ctx.now();
4066
4184
  this.ctx.db.run("DELETE FROM remote_permissions WHERE expires_at < ?", now - REMOTE_PERMISSION_KEEP_SECONDS);
4067
4185
  return this.ctx.db.run(
4068
- "INSERT OR IGNORE INTO remote_permissions (session_id, action_id, aad_id, tool, options_json, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
4186
+ "INSERT OR IGNORE INTO remote_permissions (session_id, action_id, aad_id, tool, options_json, expires_at, created_at, ask_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
4069
4187
  p.sessionId,
4070
4188
  p.actionId,
4071
4189
  p.userId,
4072
4190
  p.tool,
4073
4191
  pyJsonDumps(p.options),
4074
4192
  p.expiresAt,
4075
- now
4193
+ now,
4194
+ p.ask ? JSON.stringify(p.ask) : ""
4076
4195
  ) === 1;
4077
4196
  }
4078
4197
  getRemotePermission(sessionId, actionId) {
@@ -4099,17 +4218,21 @@ var SqliteRemotePermissionRepository = class {
4099
4218
  );
4100
4219
  return new Set(rows.map((r) => col.str(r, "session_id")));
4101
4220
  }
4221
+ setRemotePermissionMessage(sessionId, actionId, messageId) {
4222
+ this.ctx.db.run(
4223
+ "UPDATE remote_permissions SET message_id = ? WHERE session_id = ? AND action_id = ?",
4224
+ messageId,
4225
+ sessionId,
4226
+ actionId
4227
+ );
4228
+ }
4102
4229
  forgetRemotePermission(sessionId, actionId) {
4103
4230
  this.ctx.db.run("DELETE FROM remote_permissions WHERE session_id = ? AND action_id = ?", sessionId, actionId);
4104
4231
  }
4105
4232
  closeRemotePermission(sessionId, actionId, state, options = {}) {
4106
- const onlyIfPending = options.onlyIfPending ?? true;
4107
- return this.ctx.db.run(
4108
- `UPDATE remote_permissions SET state = ? WHERE session_id = ? AND action_id = ?${onlyIfPending ? " AND state = 'pending'" : ""}`,
4109
- state,
4110
- sessionId,
4111
- actionId
4112
- ) === 1;
4233
+ const from = options.onlyIfState ?? (options.onlyIfPending ?? true ? "pending" : null);
4234
+ const sql = "UPDATE remote_permissions SET state = ? WHERE session_id = ? AND action_id = ?";
4235
+ return from === null ? this.ctx.db.run(sql, state, sessionId, actionId) === 1 : this.ctx.db.run(`${sql} AND state = ?`, state, sessionId, actionId, from) === 1;
4113
4236
  }
4114
4237
  };
4115
4238
 
@@ -4499,6 +4622,7 @@ var SqliteStore = class {
4499
4622
  notifySubs;
4500
4623
  approvals;
4501
4624
  observe;
4625
+ groupContext;
4502
4626
  assistantCards;
4503
4627
  assistantButtons;
4504
4628
  assistantMessages;
@@ -4530,6 +4654,7 @@ var SqliteStore = class {
4530
4654
  this.notifySubs = new SqliteNotifySubscriptionRepository(ctx);
4531
4655
  this.approvals = new SqliteApprovalRepository(ctx);
4532
4656
  this.observe = new SqliteObserveRepository(ctx);
4657
+ this.groupContext = new SqliteGroupContextRepository(ctx);
4533
4658
  this.assistantCards = new SqliteAssistantCardRepository(ctx);
4534
4659
  this.assistantButtons = new SqliteAssistantButtonRepository(ctx);
4535
4660
  this.assistantMessages = new SqliteAssistantMessageRepository(ctx);
@@ -120,3 +120,7 @@ CREATE TABLE IF NOT EXISTS observe_cards (
120
120
  tool TEXT NOT NULL, args TEXT NOT NULL, message TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT 'open',
121
121
  created_at REAL NOT NULL);
122
122
  CREATE INDEX IF NOT EXISTS observe_cards_by_suggestion ON observe_cards (aad_id, run_id, seq);
123
+ CREATE TABLE IF NOT EXISTS group_context_off (
124
+ conversation_id TEXT PRIMARY KEY, updated_at REAL NOT NULL);
125
+ CREATE TABLE IF NOT EXISTS bound_users (
126
+ aad_id TEXT PRIMARY KEY, claw_user_id TEXT NOT NULL, token_hash TEXT NOT NULL, updated_at REAL NOT NULL);
@@ -1,15 +1,33 @@
1
- import { type IdentityLookup, type KeyOwner } from "@primusclaw/personal-agent";
1
+ import { type IdentityLookup, type KeyOwner, type Logger } from "@primusclaw/personal-agent";
2
2
  import { type Fetch } from "./http.js";
3
+ /** Where a lookup asks besides the platform, and where it says that an answer was not 200. */
4
+ export interface IdentityLookupOptions {
5
+ /**
6
+ * Claw's API without a trailing slash (Settings.clawApiUrl). An exchanged identity token is asked about here, at
7
+ * `GET /v1/me`: it is a token for Claw's audience, which the platform's `/users/self` refuses (401). Without it a
8
+ * token has no owner.
9
+ */
10
+ readonly clawApiUrl?: string;
11
+ /** Where an answer other than 200 is noted: its status, the route and the kind of credential, never the credential. */
12
+ readonly logger?: Logger;
13
+ }
3
14
  /**
4
- * Who a key belongs to, from the platform's `/users/self` asked with the key itself (the Python worker's `safe.whoami`,
5
- * `safe.safe_user` and `safe.user_id_of`, which read the same answer). Any failure is no owner: an answer other than
6
- * 200, a timeout or a broken connection, a body that is not JSON or not an object. The caller says so, or tries again
7
- * later (the subscriptions' timer that fills in Claw user ids).
15
+ * Who a credential belongs to. A person's own key (`ak-...`) is asked of the platform's `/users/self` with the key itself
16
+ * (the Python worker's `safe.whoami`, `safe.safe_user` and `safe.user_id_of`, which read the same answer). An identity
17
+ * token exchanged for them (TEAMS_EXCHANGE_FOR_CHAT) is asked of Claw's `/v1/me` with the token, when Claw's URL is
18
+ * given: the platform does not take such a token on its data routes, and Claw answers with the platform's id, name and
19
+ * email of the token's subject (it has no account type: ""). Any failure is no owner: an answer other than 200, a timeout
20
+ * or a broken connection, a body that is not JSON or not an object. The caller says so, or tries again later (the
21
+ * subscriptions' timer that fills in Claw user ids).
8
22
  */
9
23
  export declare class HttpIdentityLookup implements IdentityLookup {
10
24
  private readonly timeoutMs;
11
- private readonly http;
25
+ private readonly safe;
26
+ private readonly claw;
27
+ private readonly logger;
12
28
  /** `safeApiUrl` is the platform API without a trailing slash (Settings.safeApiUrl). */
13
- constructor(safeApiUrl: string, fetchImpl?: Fetch, timeoutMs?: number);
29
+ constructor(safeApiUrl: string, fetchImpl?: Fetch, timeoutMs?: number, options?: IdentityLookupOptions);
14
30
  ownerOfKey(key: string): Promise<KeyOwner | undefined>;
31
+ /** The JSON answer of `GET what` asked with `key`; undefined on any failure. */
32
+ private ask;
15
33
  }
@@ -1,5 +1,5 @@
1
1
  export { createClawClient, DEFAULT_CLIENT_LABEL, type ClawClientOptions } from "./client.js";
2
2
  export type { Fetch } from "./http.js";
3
- export { HttpIdentityLookup } from "./identity-lookup.js";
3
+ export { HttpIdentityLookup, type IdentityLookupOptions } from "./identity-lookup.js";
4
4
  export { HttpKeyRevoker } from "./key-revoker.js";
5
5
  export { ruleText } from "./rule.js";
@@ -10,6 +10,8 @@ export declare class HttpRemoteAgents implements ClawRemoteAgentsPort {
10
10
  connectors(key: ClawCredential, options?: {
11
11
  includeHidden?: boolean;
12
12
  }): Promise<RemoteConnector[]>;
13
+ approvals(key: ClawCredential): Promise<boolean>;
14
+ setApprovals(key: ClawCredential, enabled: boolean): Promise<boolean>;
13
15
  pair(key: ClawCredential): Promise<RemotePairing>;
14
16
  hide(key: ClawCredential, connectorId: string, refs: readonly string[], hidden: boolean): Promise<RemoteHidden>;
15
17
  revoke(key: ClawCredential, connectorId: string): Promise<void>;
@@ -4,6 +4,8 @@ import { type Env } from "./env.js";
4
4
  export declare const GITHUB_CLI_CLIENT_ID = "178c6fc778ccc68e1d6a";
5
5
  /** The default of ASSISTANT_REPLY_WINDOW_SECONDS. */
6
6
  export declare const ASSISTANT_REPLY_WINDOW_SECONDS: number;
7
+ /** The most messages TEAMS_GROUP_CONTEXT_MESSAGES may keep per group chat. */
8
+ export declare const MAX_GROUP_CONTEXT_MESSAGES = 200;
7
9
  /**
8
10
  * The worker's channel-neutral settings from the environment (the Python worker's `Settings.from_env`, without the
9
11
  * Teams fields). `secrets` are what the deployment's SecretSource loaded; a name it holds is taken from there and never
@@ -0,0 +1,9 @@
1
+ /** GroupContextRepository on SQLite (table `group_context_off`: one row per chat that turned the context off). */
2
+ import type { GroupContextRepository } from "@primusclaw/personal-agent";
3
+ import type { StoreContext } from "./context.js";
4
+ export declare class SqliteGroupContextRepository implements GroupContextRepository {
5
+ private readonly ctx;
6
+ constructor(ctx: StoreContext);
7
+ contextOff(conversationId: string): boolean;
8
+ setContextOff(conversationId: string, off: boolean): void;
9
+ }
@@ -1,4 +1,3 @@
1
- /** LinkRepository on SQLite (tables `links`, `pending_keys`, `link_attempts`). */
2
1
  import { type LinkLimits, type LinkRepository, type RedeemOutcome } from "@primusclaw/personal-agent";
3
2
  import { type StoreContext, type SecurityEventSink } from "./context.js";
4
3
  export declare class SqliteLinkRepository implements LinkRepository {
@@ -19,7 +18,16 @@ export declare class SqliteLinkRepository implements LinkRepository {
19
18
  getKey(userId: string): string | null;
20
19
  unlink(userId: string): void;
21
20
  setClawUserId(userId: string, clawUserId: string): void;
21
+ boundClawUser(userId: string, credential: string): {
22
+ clawUserId: string;
23
+ current: boolean;
24
+ } | null;
25
+ /** Ignored for a linked person (a link's id is read for its key). Another account than before forgets the subscription. */
26
+ setBoundClawUser(userId: string, clawUserId: string, credential: string): void;
27
+ /** Ignored for a linked person: their link keeps its own subscription. */
28
+ forgetBound(userId: string): void;
22
29
  needsClawUserId(userId: string): boolean;
30
+ /** The link's (a linked person's id is never taken from anywhere else), else the bound person's. */
23
31
  clawUserIdOf(userId: string): string | null;
24
32
  linkedUsers(): string[];
25
33
  userForClawUser(clawUserId: string): string | null;
@@ -17,8 +17,10 @@ export declare class SqliteRemotePermissionRepository implements RemotePermissio
17
17
  getRemotePermission(sessionId: string, actionId: string): RemotePermission | null;
18
18
  pendingRemotePermissions(userId: string, sessionId: string): RemotePermission[];
19
19
  waitingRemoteSessions(userId: string): Set<string>;
20
+ setRemotePermissionMessage(sessionId: string, actionId: string, messageId: string): void;
20
21
  forgetRemotePermission(sessionId: string, actionId: string): void;
21
22
  closeRemotePermission(sessionId: string, actionId: string, state: RemotePermissionState, options?: {
22
23
  readonly onlyIfPending?: boolean;
24
+ readonly onlyIfState?: RemotePermissionState;
23
25
  }): boolean;
24
26
  }
@@ -13,6 +13,7 @@ import { SqliteLinkRepository } from "./link-repository.js";
13
13
  import { SqliteMemberDirectoryRepository } from "./member-repository.js";
14
14
  import { SqliteNotifySubscriptionRepository } from "./notify-repository.js";
15
15
  import { SqliteObserveRepository } from "./observe-repository.js";
16
+ import { SqliteGroupContextRepository } from "./group-context-repository.js";
16
17
  import { SqliteOnboardingRepository } from "./onboarding-repository.js";
17
18
  import { SqliteConversationRefRepository, SqliteHeldEventRepository, SqlitePrefsRepository } from "./people-repository.js";
18
19
  import { type ReencryptCounts } from "./reencrypt.js";
@@ -45,6 +46,7 @@ export declare class SqliteStore implements StorePort {
45
46
  readonly notifySubs: SqliteNotifySubscriptionRepository;
46
47
  readonly approvals: SqliteApprovalRepository;
47
48
  readonly observe: SqliteObserveRepository;
49
+ readonly groupContext: SqliteGroupContextRepository;
48
50
  readonly assistantCards: SqliteAssistantCardRepository;
49
51
  readonly assistantButtons: SqliteAssistantButtonRepository;
50
52
  readonly assistantMessages: SqliteAssistantMessageRepository;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primusclaw/personal-agent-runtime",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Node adapters for the personal agent's ports: clock, randomness, logging (and, as the port lands, storage, Claw, metrics, HTTP).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@primusclaw/personal-agent": "0.1.1",
32
+ "@primusclaw/personal-agent": "0.3.0",
33
33
  "fastify": "^5.12.8",
34
34
  "zod": "^4.6.7"
35
35
  }