@primusclaw/personal-agent-runtime 0.1.0 → 0.2.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;
@@ -3608,6 +3653,15 @@ var ADDED_COLUMNS = [
3608
3653
  ]
3609
3654
  ],
3610
3655
  ["notify_subs", [["types_version", "INTEGER NOT NULL DEFAULT 0"]]],
3656
+ [
3657
+ "remote_permissions",
3658
+ [
3659
+ ["message_id", "TEXT NOT NULL DEFAULT ''"],
3660
+ // '' = the channel never said the card's id
3661
+ ["ask_json", "TEXT NOT NULL DEFAULT ''"]
3662
+ // '' = not a question a terminal asks
3663
+ ]
3664
+ ],
3611
3665
  [
3612
3666
  "turn_answers",
3613
3667
  [
@@ -3841,6 +3895,26 @@ var SqliteObserveRepository = class {
3841
3895
  }
3842
3896
  };
3843
3897
 
3898
+ // packages/runtime/src/sqlite/group-context-repository.ts
3899
+ var SqliteGroupContextRepository = class {
3900
+ constructor(ctx) {
3901
+ this.ctx = ctx;
3902
+ }
3903
+ ctx;
3904
+ contextOff(conversationId) {
3905
+ return this.ctx.db.get("SELECT 1 AS off FROM group_context_off WHERE conversation_id = ?", conversationId) !== void 0;
3906
+ }
3907
+ setContextOff(conversationId, off) {
3908
+ if (off) {
3909
+ this.ctx.db.run(
3910
+ "INSERT INTO group_context_off (conversation_id, updated_at) VALUES (?, ?) ON CONFLICT (conversation_id) DO UPDATE SET updated_at = excluded.updated_at",
3911
+ conversationId,
3912
+ this.ctx.now()
3913
+ );
3914
+ } else this.ctx.db.run("DELETE FROM group_context_off WHERE conversation_id = ?", conversationId);
3915
+ }
3916
+ };
3917
+
3844
3918
  // packages/runtime/src/sqlite/people-repository.ts
3845
3919
  import {
3846
3920
  DEFAULT_QUIET,
@@ -4044,7 +4118,7 @@ var SqliteRemoteSessionNameRepository = class {
4044
4118
  );
4045
4119
  }
4046
4120
  };
4047
- var PERMISSION_COLUMNS = "session_id, action_id, aad_id, tool, options_json, expires_at, state";
4121
+ var PERMISSION_COLUMNS = "session_id, action_id, aad_id, tool, options_json, expires_at, state, message_id, ask_json";
4048
4122
  function permission(row) {
4049
4123
  return {
4050
4124
  sessionId: col.str(row, "session_id"),
@@ -4053,9 +4127,20 @@ function permission(row) {
4053
4127
  tool: col.str(row, "tool"),
4054
4128
  options: jsonList(col.str(row, "options_json")),
4055
4129
  expiresAt: col.num(row, "expires_at"),
4056
- state: col.str(row, "state")
4130
+ state: col.str(row, "state"),
4131
+ messageId: col.str(row, "message_id"),
4132
+ ask: askOf(col.str(row, "ask_json"))
4057
4133
  };
4058
4134
  }
4135
+ function askOf(text3) {
4136
+ if (!text3) return null;
4137
+ try {
4138
+ const value = JSON.parse(text3);
4139
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
4140
+ } catch {
4141
+ return null;
4142
+ }
4143
+ }
4059
4144
  var SqliteRemotePermissionRepository = class {
4060
4145
  constructor(ctx) {
4061
4146
  this.ctx = ctx;
@@ -4065,14 +4150,15 @@ var SqliteRemotePermissionRepository = class {
4065
4150
  const now = this.ctx.now();
4066
4151
  this.ctx.db.run("DELETE FROM remote_permissions WHERE expires_at < ?", now - REMOTE_PERMISSION_KEEP_SECONDS);
4067
4152
  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 (?, ?, ?, ?, ?, ?, ?)",
4153
+ "INSERT OR IGNORE INTO remote_permissions (session_id, action_id, aad_id, tool, options_json, expires_at, created_at, ask_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
4069
4154
  p.sessionId,
4070
4155
  p.actionId,
4071
4156
  p.userId,
4072
4157
  p.tool,
4073
4158
  pyJsonDumps(p.options),
4074
4159
  p.expiresAt,
4075
- now
4160
+ now,
4161
+ p.ask ? JSON.stringify(p.ask) : ""
4076
4162
  ) === 1;
4077
4163
  }
4078
4164
  getRemotePermission(sessionId, actionId) {
@@ -4099,17 +4185,21 @@ var SqliteRemotePermissionRepository = class {
4099
4185
  );
4100
4186
  return new Set(rows.map((r) => col.str(r, "session_id")));
4101
4187
  }
4188
+ setRemotePermissionMessage(sessionId, actionId, messageId) {
4189
+ this.ctx.db.run(
4190
+ "UPDATE remote_permissions SET message_id = ? WHERE session_id = ? AND action_id = ?",
4191
+ messageId,
4192
+ sessionId,
4193
+ actionId
4194
+ );
4195
+ }
4102
4196
  forgetRemotePermission(sessionId, actionId) {
4103
4197
  this.ctx.db.run("DELETE FROM remote_permissions WHERE session_id = ? AND action_id = ?", sessionId, actionId);
4104
4198
  }
4105
4199
  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;
4200
+ const from = options.onlyIfState ?? (options.onlyIfPending ?? true ? "pending" : null);
4201
+ const sql = "UPDATE remote_permissions SET state = ? WHERE session_id = ? AND action_id = ?";
4202
+ 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
4203
  }
4114
4204
  };
4115
4205
 
@@ -4499,6 +4589,7 @@ var SqliteStore = class {
4499
4589
  notifySubs;
4500
4590
  approvals;
4501
4591
  observe;
4592
+ groupContext;
4502
4593
  assistantCards;
4503
4594
  assistantButtons;
4504
4595
  assistantMessages;
@@ -4530,6 +4621,7 @@ var SqliteStore = class {
4530
4621
  this.notifySubs = new SqliteNotifySubscriptionRepository(ctx);
4531
4622
  this.approvals = new SqliteApprovalRepository(ctx);
4532
4623
  this.observe = new SqliteObserveRepository(ctx);
4624
+ this.groupContext = new SqliteGroupContextRepository(ctx);
4533
4625
  this.assistantCards = new SqliteAssistantCardRepository(ctx);
4534
4626
  this.assistantButtons = new SqliteAssistantButtonRepository(ctx);
4535
4627
  this.assistantMessages = new SqliteAssistantMessageRepository(ctx);
@@ -120,3 +120,5 @@ 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);
@@ -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
+ }
@@ -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.0",
3
+ "version": "0.2.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.0",
32
+ "@primusclaw/personal-agent": "0.2.0",
33
33
  "fastify": "^5.12.8",
34
34
  "zod": "^4.6.7"
35
35
  }