@primusclaw/personal-agent-runtime 0.0.0-stage → 0.1.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.
Files changed (76) hide show
  1. package/dist/LICENSE +37 -0
  2. package/dist/index.js +5734 -0
  3. package/dist/sqlite/schema.sql +122 -0
  4. package/dist/types/claw/answers.d.ts +11 -0
  5. package/dist/types/claw/assistant.d.ts +27 -0
  6. package/dist/types/claw/client.d.ts +23 -0
  7. package/dist/types/claw/http.d.ts +64 -0
  8. package/dist/types/claw/identity-lookup.d.ts +15 -0
  9. package/dist/types/claw/index.d.ts +5 -0
  10. package/dist/types/claw/key-revoker.d.ts +22 -0
  11. package/dist/types/claw/remote-agents.d.ts +23 -0
  12. package/dist/types/claw/rule.d.ts +20 -0
  13. package/dist/types/claw/sessions.d.ts +56 -0
  14. package/dist/types/claw/settings.d.ts +46 -0
  15. package/dist/types/claw/sse-stream.d.ts +14 -0
  16. package/dist/types/claw/work-items.d.ts +22 -0
  17. package/dist/types/claw/zip.d.ts +15 -0
  18. package/dist/types/config/env.d.ts +29 -0
  19. package/dist/types/config/index.d.ts +3 -0
  20. package/dist/types/config/secrets.d.ts +21 -0
  21. package/dist/types/config/settings.d.ts +13 -0
  22. package/dist/types/context/als-request-scope.d.ts +3 -0
  23. package/dist/types/crypto/fernet.d.ts +37 -0
  24. package/dist/types/crypto/link-code.d.ts +6 -0
  25. package/dist/types/crypto/node-crypto.d.ts +2 -0
  26. package/dist/types/crypto/py-base64.d.ts +8 -0
  27. package/dist/types/crypto-random.d.ts +3 -0
  28. package/dist/types/exchange/index.d.ts +2 -0
  29. package/dist/types/exchange/serial.d.ts +8 -0
  30. package/dist/types/exchange/token-exchange.d.ts +56 -0
  31. package/dist/types/github/api.d.ts +44 -0
  32. package/dist/types/github/app-token.d.ts +46 -0
  33. package/dist/types/github/credential-client.d.ts +37 -0
  34. package/dist/types/github/device-flow.d.ts +28 -0
  35. package/dist/types/github/feedback-config.d.ts +26 -0
  36. package/dist/types/github/feedback-issues.d.ts +23 -0
  37. package/dist/types/github/feedback-worker.d.ts +11 -0
  38. package/dist/types/github/index.d.ts +7 -0
  39. package/dist/types/github/intent-reader.d.ts +28 -0
  40. package/dist/types/github/pull-reader.d.ts +22 -0
  41. package/dist/types/http/claw-events.d.ts +27 -0
  42. package/dist/types/http/link.d.ts +14 -0
  43. package/dist/types/http/routes.d.ts +37 -0
  44. package/dist/types/http/server.d.ts +24 -0
  45. package/dist/types/index.d.ts +29 -0
  46. package/dist/types/line-logger.d.ts +15 -0
  47. package/dist/types/logging/chat-log.d.ts +26 -0
  48. package/dist/types/metrics/exposition.d.ts +17 -0
  49. package/dist/types/metrics/index.d.ts +2 -0
  50. package/dist/types/metrics/prometheus-metrics.d.ts +46 -0
  51. package/dist/types/sqlite/approval-repository.d.ts +19 -0
  52. package/dist/types/sqlite/assistant-repository.d.ts +42 -0
  53. package/dist/types/sqlite/context.d.ts +38 -0
  54. package/dist/types/sqlite/database.d.ts +37 -0
  55. package/dist/types/sqlite/feedback-repository.d.ts +27 -0
  56. package/dist/types/sqlite/forget.d.ts +6 -0
  57. package/dist/types/sqlite/github-repository.d.ts +36 -0
  58. package/dist/types/sqlite/idempotency-repository.d.ts +14 -0
  59. package/dist/types/sqlite/link-repository.d.ts +27 -0
  60. package/dist/types/sqlite/member-repository.d.ts +15 -0
  61. package/dist/types/sqlite/migrations.d.ts +12 -0
  62. package/dist/types/sqlite/notify-repository.d.ts +17 -0
  63. package/dist/types/sqlite/observe-repository.d.ts +19 -0
  64. package/dist/types/sqlite/onboarding-repository.d.ts +20 -0
  65. package/dist/types/sqlite/open.d.ts +25 -0
  66. package/dist/types/sqlite/people-repository.d.ts +33 -0
  67. package/dist/types/sqlite/py-json.d.ts +16 -0
  68. package/dist/types/sqlite/reencrypt.d.ts +13 -0
  69. package/dist/types/sqlite/remote-repository.d.ts +24 -0
  70. package/dist/types/sqlite/remote-state-repository.d.ts +20 -0
  71. package/dist/types/sqlite/session-repository.d.ts +31 -0
  72. package/dist/types/sqlite/store.d.ts +67 -0
  73. package/dist/types/sqlite/turn-repository.d.ts +24 -0
  74. package/dist/types/system-clock.d.ts +3 -0
  75. package/package.json +34 -4
  76. package/README.md +0 -3
@@ -0,0 +1,2 @@
1
+ import type { Crypto } from "@primusclaw/personal-agent";
2
+ export declare const nodeCrypto: Crypto;
@@ -0,0 +1,8 @@
1
+ /** The data was not base64 Python would read (`binascii.Error`). */
2
+ export declare class Base64Error extends Error {
3
+ readonly name = "Base64Error";
4
+ }
5
+ /** Python's `base64.urlsafe_b64decode` (binascii non-strict mode). */
6
+ export declare function urlsafeB64Decode(input: string | Uint8Array): Uint8Array;
7
+ /** Python's `base64.urlsafe_b64encode`: padded, `-_` alphabet. */
8
+ export declare function urlsafeB64Encode(data: Uint8Array): string;
@@ -0,0 +1,3 @@
1
+ import type { Random } from "@primusclaw/personal-agent";
2
+ /** Randomness from the operating system's CSPRNG (`node:crypto`). */
3
+ export declare const cryptoRandom: Random;
@@ -0,0 +1,2 @@
1
+ /** The identity service's token exchange and binding requests (exchange.py), as core's TokenExchange plugin. */
2
+ export * from "./token-exchange.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * One task at a time per key, in the order they asked (an asyncio.Lock per key): a later task runs once the earlier
3
+ * one has finished, whatever its outcome, and sees what it left behind.
4
+ */
5
+ export declare class PerKeySerial {
6
+ private readonly tails;
7
+ run<T>(key: string, task: () => Promise<T>): Promise<T>;
8
+ }
@@ -0,0 +1,56 @@
1
+ import { type BindingRequest, type Clock, type ExchangeError, type ExchangeFailed, type ExchangeSettings, type ExchangeUnavailable, type Result, type TokenExchange } from "@primusclaw/personal-agent";
2
+ export declare const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
3
+ export declare const USER_REF = "urn:primus:params:oauth:token-type:user-ref";
4
+ export declare const ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
5
+ /** A token with less than this left is never handed out. */
6
+ export declare const REFRESH_MARGIN_SECONDS = 60;
7
+ /** How long "not bound" is remembered, so chatter does not hammer the identity service. */
8
+ export declare const NOT_BOUND_TTL_SECONDS = 15;
9
+ /** A confirm link with less than this left is not handed out again. */
10
+ export declare const REQUEST_MARGIN_SECONDS = 60;
11
+ type Fetch = typeof fetch;
12
+ /** The JSON object of an answer ({} when it is not one). */
13
+ /** How long an exchange or a binding request may take, answer included. */
14
+ export declare const EXCHANGE_TIMEOUT_MS = 30000;
15
+ /**
16
+ * Short-lived tokens from the identity service's OAuth 2.0 token exchange (RFC 8693). The worker is a service client:
17
+ * for a channel user with a confirmed binding (issuer + their user id) it trades its own client credentials for a token
18
+ * that acts as that person for about 10 minutes. Nothing long-lived is kept. A user with no binding gets `not_bound`
19
+ * (remembered briefly) and is offered a binding request: a one-time link they open, sign in at and confirm once.
20
+ */
21
+ export declare class HttpTokenExchange implements TokenExchange {
22
+ private readonly settings;
23
+ private readonly clock;
24
+ private readonly fetchFn;
25
+ /** The whole call, answer included (the Python worker's HTTP client had 30 s). */
26
+ private readonly timeoutMs;
27
+ readonly enabled = true;
28
+ /** user -> (token, usable until) */
29
+ private readonly tokens;
30
+ /** user -> remembered until */
31
+ private readonly unbound;
32
+ /** user -> (open binding request, usable until) */
33
+ private readonly requests;
34
+ private readonly exchanges;
35
+ private readonly bindings;
36
+ private readonly authorization;
37
+ constructor(settings: ExchangeSettings, clock: Clock, fetchFn?: Fetch,
38
+ /** The whole call, answer included (the Python worker's HTTP client had 30 s). */
39
+ timeoutMs?: number);
40
+ private now;
41
+ invalidate(userId: string): void;
42
+ private cachedToken;
43
+ /** A token acting as the person bound to `userId` (one exchange in flight per user). */
44
+ tokenFor(userId: string): Promise<Result<string, ExchangeError>>;
45
+ private exchange;
46
+ private cachedRequest;
47
+ /**
48
+ * The confirm link for `userId`. A newer request withdraws the older link at the identity service, so the one still
49
+ * open is handed out again. `email`, when the channel supplied one, restricts who can confirm it.
50
+ */
51
+ bindingRequest(userId: string, email?: string): Promise<Result<BindingRequest, ExchangeFailed | ExchangeUnavailable>>;
52
+ /** The binding endpoint: as configured, else beside the token endpoint (.../oauth/token -> .../oauth/binding-requests). */
53
+ bindingUrl(): string;
54
+ private requestBinding;
55
+ }
56
+ export {};
@@ -0,0 +1,44 @@
1
+ export declare const GITHUB_API = "https://api.github.com";
2
+ export declare const GITHUB_HEADERS: Readonly<Record<string, string>>;
3
+ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
4
+ /** GitHub refused, or could not be reached (status 0). `code` never carries a body or a secret. */
5
+ export declare class GithubApiError extends Error {
6
+ readonly status: number;
7
+ readonly code: string;
8
+ constructor(status: number, code?: string);
9
+ }
10
+ /** Where the token of an authenticated call comes from. */
11
+ export interface GithubTokenSource {
12
+ token(): Promise<string>;
13
+ /** A token the server no longer accepts: the next `token()` mints or reads a new one. */
14
+ invalidate(): void;
15
+ }
16
+ export interface GithubRequest {
17
+ readonly method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
18
+ /** The path under the API root, starting with "/" (query included). */
19
+ readonly path: string;
20
+ readonly body?: unknown;
21
+ readonly timeoutMs?: number;
22
+ }
23
+ export interface GithubApiOptions {
24
+ readonly fetch?: FetchLike;
25
+ readonly apiUrl?: string;
26
+ /** None: calls are anonymous (public reads only). */
27
+ readonly tokens?: GithubTokenSource;
28
+ readonly timeoutMs?: number;
29
+ }
30
+ export declare class GithubApi {
31
+ readonly apiUrl: string;
32
+ private readonly fetchImpl;
33
+ private readonly tokens;
34
+ private readonly timeoutMs;
35
+ constructor(options?: GithubApiOptions);
36
+ /**
37
+ * One call. With a token source, a 401 invalidates the cached token and the call is made once more with a fresh one. A
38
+ * transport failure is GithubApiError(0, "transport_<name>"); any HTTP answer is returned to the caller to read.
39
+ */
40
+ request(request: GithubRequest): Promise<Response>;
41
+ private once;
42
+ }
43
+ /** A path segment as GitHub's URLs take it (`quote(x, safe='')`). */
44
+ export declare function segment(value: string): string;
@@ -0,0 +1,46 @@
1
+ import { type FetchLike, type GithubTokenSource } from "./api.js";
2
+ /** No usable GitHub credential right now. `code` says why and never carries a secret. */
3
+ export declare class GithubAuthError extends Error {
4
+ readonly code: string;
5
+ constructor(code: string);
6
+ }
7
+ /** The JWT a GitHub App signs to prove itself (valid 10 minutes at most; iat is back-dated for clock skew). */
8
+ export declare function appJwt(appId: string, keyPem: string | Buffer, nowSeconds: number): string;
9
+ export interface AppTokenOptions {
10
+ readonly appId: string;
11
+ readonly installationId: string;
12
+ /** The App's private key (PEM), read when a token is minted. */
13
+ readonly key: () => Promise<string | Buffer>;
14
+ /** The repository names (without owner) the minted token is limited to. */
15
+ readonly repositories: readonly string[];
16
+ /** The permissions the minted token is limited to, e.g. `{ issues: "write" }`. */
17
+ readonly permissions: Readonly<Record<string, string>>;
18
+ readonly fetch?: FetchLike;
19
+ readonly apiUrl?: string;
20
+ /** Seconds since the epoch. */
21
+ readonly clock?: () => number;
22
+ }
23
+ export declare class AppTokenSource implements GithubTokenSource {
24
+ private readonly options;
25
+ private cached;
26
+ private minting;
27
+ private readonly fetchImpl;
28
+ private readonly apiUrl;
29
+ private readonly clock;
30
+ constructor(options: AppTokenOptions);
31
+ invalidate(): void;
32
+ token(): Promise<string>;
33
+ private mint;
34
+ }
35
+ /** A plain token, read when it is used (a mounted file or a secret); an App token first, when there is one. */
36
+ export declare class FallbackTokenSource implements GithubTokenSource {
37
+ private readonly app;
38
+ private readonly plain;
39
+ private readonly clock;
40
+ private readonly appRetrySeconds;
41
+ private readonly onAppFailure;
42
+ private appFailedAt;
43
+ constructor(app: GithubTokenSource | undefined, plain: (() => Promise<string>) | undefined, clock?: () => number, appRetrySeconds?: number, onAppFailure?: (code: string) => void);
44
+ invalidate(): void;
45
+ token(): Promise<string>;
46
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The identity service's `/oauth/external-credentials` calls, made as the worker's service client for a person it holds a
3
+ * binding for (the same client and the same `external:<issuer>|<directory id>` reference as the token exchange). A person
4
+ * with no binding is `not_bound`. Nothing of an answer is kept but its fields: a request that carries a token must not be
5
+ * able to put it in a log line.
6
+ */
7
+ import { type CredentialFailure, type GithubCredentials, type GithubState, type Result } from "@primusclaw/personal-agent";
8
+ import { z } from "zod";
9
+ import type { FetchLike } from "./api.js";
10
+ declare const answer: z.ZodObject<{
11
+ user_id: z.ZodOptional<z.ZodUnknown>;
12
+ item: z.ZodOptional<z.ZodUnknown>;
13
+ bound: z.ZodOptional<z.ZodUnknown>;
14
+ unbound: z.ZodOptional<z.ZodUnknown>;
15
+ error: z.ZodOptional<z.ZodUnknown>;
16
+ }, z.core.$loose>;
17
+ type Answer = z.infer<typeof answer>;
18
+ export declare function stateOf(data: Answer): GithubState;
19
+ export interface CredentialClientOptions {
20
+ readonly baseUrl: string;
21
+ readonly clientId: string;
22
+ readonly clientSecret: string;
23
+ readonly issuer: string;
24
+ readonly fetch?: FetchLike;
25
+ }
26
+ export declare class IdentityCredentialClient implements GithubCredentials {
27
+ private readonly options;
28
+ private readonly base;
29
+ private readonly fetchImpl;
30
+ constructor(options: CredentialClientOptions);
31
+ private call;
32
+ status(userId: string): Promise<Result<GithubState, CredentialFailure>>;
33
+ bind(userId: string, token: string): Promise<Result<GithubState, CredentialFailure>>;
34
+ unbind(userId: string): Promise<Result<boolean, CredentialFailure>>;
35
+ setDelegation(userId: string, enabled: boolean): Promise<Result<GithubState, CredentialFailure>>;
36
+ }
37
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * RFC 8628 against github.com with a public client (no secret); the scope asked for is always `repo`. Nothing GitHub
3
+ * answers is kept but the fields read: a failure is only its reason, never a body, a code or a token.
4
+ */
5
+ import { type Clock, type DeviceCode, type DeviceFlow, type DeviceFlowCancelled, type DeviceFlowFailed, type Result } from "@primusclaw/personal-agent";
6
+ import type { FetchLike } from "./api.js";
7
+ export declare const DEVICE_CODE_URL = "https://github.com/login/device/code";
8
+ export declare const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
9
+ export declare const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
10
+ export declare const MAX_DEVICE_WAIT_SECONDS: number;
11
+ export interface DeviceFlowOptions {
12
+ readonly clientId: string;
13
+ readonly clock: Clock;
14
+ readonly fetch?: FetchLike;
15
+ }
16
+ export declare class GithubDeviceFlow implements DeviceFlow {
17
+ private readonly options;
18
+ private readonly fetchImpl;
19
+ constructor(options: DeviceFlowOptions);
20
+ private post;
21
+ start(): Promise<Result<DeviceCode, DeviceFlowFailed>>;
22
+ /**
23
+ * Polls until the person approves: the token, or why not. Honours `interval` and `slow_down`; gives up when the code runs
24
+ * out (or after MAX_DEVICE_WAIT_SECONDS, whichever is first).
25
+ */
26
+ waitForToken(code: DeviceCode, signal?: AbortSignal): Promise<Result<string, DeviceFlowFailed | DeviceFlowCancelled>>;
27
+ private poll;
28
+ }
@@ -0,0 +1,26 @@
1
+ import { type FeedbackSink, type Logger } from "@primusclaw/personal-agent";
2
+ import { type FetchLike } from "./api.js";
3
+ export interface FeedbackConfig {
4
+ /** `owner/name` ("" when none). */
5
+ readonly repo: string;
6
+ readonly appId: string;
7
+ readonly installationId: string;
8
+ /** The App's private key (PEM) as a mounted file; "" when the key came from a secret store (`keyPem`). */
9
+ readonly keyFile: string;
10
+ /** The same key read from a secret store at start, held in memory only. */
11
+ readonly keyPem: string;
12
+ readonly tokenFile: string;
13
+ readonly apiUrl: string;
14
+ }
15
+ export declare function feedbackConfigFromEnv(env: Readonly<Record<string, string | undefined>>, keyPem?: string): FeedbackConfig;
16
+ export declare function appConfigured(config: FeedbackConfig): boolean;
17
+ /** A repository and some credential; whether the credential works is only known when it is used. */
18
+ export declare function feedbackConfigured(config: FeedbackConfig): boolean;
19
+ export interface FeedbackSinkOptions {
20
+ readonly fetch?: FetchLike;
21
+ /** Seconds since the epoch. */
22
+ readonly clock?: () => number;
23
+ readonly log: Logger;
24
+ }
25
+ /** The sink feedback delivers to, or undefined when nothing is configured (reports are then only recorded). */
26
+ export declare function feedbackSink(config: FeedbackConfig, options: FeedbackSinkOptions): FeedbackSink | undefined;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Feedback's issues in one GitHub repository (the Python worker's `feedback.GitHubIssues`): the FeedbackSink core
3
+ * delivers reports to. Every call goes through the shared GitHub client (`api.ts`: its headers, one retry with a fresh
4
+ * token on a 401); a failure is a status and a short code, never a body or a secret.
5
+ */
6
+ import { type FeedbackSink, type IssueRef, type Logger, type Result, type SinkError } from "@primusclaw/personal-agent";
7
+ import { GithubApi } from "./api.js";
8
+ export declare const FEEDBACK_LABEL = "clawy-feedback";
9
+ export declare class GithubIssueSink implements FeedbackSink {
10
+ private readonly api;
11
+ private readonly log;
12
+ private labelReady;
13
+ private readonly repoPath;
14
+ constructor(api: GithubApi,
15
+ /** `owner/name`. */
16
+ repo: string, log: Logger);
17
+ createIssue(title: string, body: string, labels: readonly string[]): Promise<Result<IssueRef, SinkError>>;
18
+ findIssue(marker: string, sinceEpochMs: number): Promise<Result<IssueRef | undefined, SinkError>>;
19
+ hasComment(issue: number, marker: string, sinceEpochMs: number): Promise<Result<boolean, SinkError>>;
20
+ comment(issue: number, body: string): Promise<Result<void, SinkError>>;
21
+ /** The feedback label, made once if the repository lacks it. Best effort: a report is worth more than its label. */
22
+ private ensureLabel;
23
+ }
@@ -0,0 +1,11 @@
1
+ import { type WorkerInfo } from "@primusclaw/personal-agent";
2
+ /**
3
+ * The `version` of a package.json (the worker's own package: whoever assembles the worker passes its URL), "unknown"
4
+ * when the file cannot be read or names no version.
5
+ */
6
+ export declare function packageVersion(packageJson: URL): string;
7
+ /** The deployment as a report names it. */
8
+ export declare function workerInfoFromEnv(env: Readonly<Record<string, string | undefined>>, worker: {
9
+ readonly pluginId: string;
10
+ readonly version: string;
11
+ }): WorkerInfo;
@@ -0,0 +1,7 @@
1
+ /** The GitHub adapters the worker's assembly injects into the review service (the ports in core's `ports/github.ts`). */
2
+ export { GITHUB_API, GithubApi, GithubApiError, type GithubApiOptions, type GithubTokenSource } from "./api.js";
3
+ export { AppTokenSource, FallbackTokenSource, GithubAuthError, appJwt, type AppTokenOptions } from "./app-token.js";
4
+ export { IdentityCredentialClient, type CredentialClientOptions } from "./credential-client.js";
5
+ export { GithubDeviceFlow, MAX_DEVICE_WAIT_SECONDS, type DeviceFlowOptions } from "./device-flow.js";
6
+ export { GatewayIntentReader, INTENT_MS, type IntentReaderOptions } from "./intent-reader.js";
7
+ export { PULL_READ_MS, PublicPullReader } from "./pull-reader.js";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * One small model call that says what a message asks for, made with the asker's own key on the platform's LLM gateway
3
+ * (OpenAI-compatible `chat/completions`). What it answers decides only what to do, and only after the domain holds it to
4
+ * the message; it widens no permission. undefined for anything but an answer in time: the rules read the message instead.
5
+ */
6
+ import { type IntentReader, type ListedMention, type Logger } from "@primusclaw/personal-agent";
7
+ import type { FetchLike } from "./api.js";
8
+ /** Past this the rules read the request instead. */
9
+ export declare const INTENT_MS = 10000;
10
+ export interface IntentReaderOptions {
11
+ readonly baseUrl: string;
12
+ readonly model: string;
13
+ readonly logger: Logger;
14
+ readonly fetch?: FetchLike;
15
+ readonly timeoutMs?: number;
16
+ }
17
+ /** The JSON object in what the model wrote; `{ intent: null }` when there is none. */
18
+ export declare function readingOf(content: unknown): Record<string, unknown>;
19
+ export declare class GatewayIntentReader implements IntentReader {
20
+ private readonly options;
21
+ private readonly url;
22
+ private readonly fetchImpl;
23
+ constructor(options: IntentReaderOptions);
24
+ read(key: string, text: string, mentions: readonly ListedMention[]): Promise<Record<string, unknown> | undefined>;
25
+ readMode(key: string, text: string): Promise<Record<string, unknown> | undefined>;
26
+ readAnswer(key: string, text: string, pending: readonly Readonly<Record<string, string>>[]): Promise<Record<string, unknown> | undefined>;
27
+ private complete;
28
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Who wrote a pull request (and its title, and the branch it goes into), read from GitHub's public API with no credential.
3
+ * It exists to refuse early (a committer cannot approve their own pull request); it is not what stops it, Claw is. So any
4
+ * answer that is not a reading (GitHub down, a rate limit, a pull request that is not there) is "not known".
5
+ */
6
+ import { type Logger, type PullRef, type PullReader } from "@primusclaw/personal-agent";
7
+ import { GithubApi } from "./api.js";
8
+ /** The one public read of who wrote a pull request: an early answer, never worth a long wait. */
9
+ export declare const PULL_READ_MS = 5000;
10
+ export declare class PublicPullReader implements PullReader {
11
+ private readonly api;
12
+ private readonly logger;
13
+ constructor(api: GithubApi, logger: Logger);
14
+ private get;
15
+ repo(owner: string, name: string): Promise<string | undefined>;
16
+ private pullPath;
17
+ base(pull: PullRef): Promise<string>;
18
+ read(pull: PullRef): Promise<{
19
+ readonly author: string | undefined;
20
+ readonly title: string;
21
+ }>;
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { Clock, ConversationRefRepository, IdempotencyRepository, JsonObject, LinkRepository, Logger, MetricsPort, NotifySubscriptionRepository } from "@primusclaw/personal-agent";
2
+ import { type HttpRoute } from "./routes.js";
3
+ /** The largest event Claw posts, with room (a say of 9000 characters with its sources, or an image carried inline). */
4
+ export declare const MAX_BODY_BYTES: number;
5
+ export declare const MAX_SKEW_SECONDS = 300;
6
+ /** A copy of an event another copy is being sent for waits this long for that send (under Claw's 5 s delivery deadline). */
7
+ export declare const BUSY_WAIT_MS = 3000;
8
+ export declare const BUSY_POLL_MS = 50;
9
+ /** What carries an event into the chat: "delivered", "held", "dropped", ... or "retry" (decided later: Claw sends it again). */
10
+ export type EventSink = (userId: string, reference: JsonObject, event: JsonObject) => Promise<string>;
11
+ export interface ClawEventsDeps {
12
+ readonly notifySubs: Pick<NotifySubscriptionRepository, "notifySubByRoute">;
13
+ readonly links: Pick<LinkRepository, "clawUserIdOf">;
14
+ readonly references: Pick<ConversationRefRepository, "getReference">;
15
+ readonly claims: Pick<IdempotencyRepository, "claimEvent" | "finishEvent" | "releaseEvent">;
16
+ readonly deliver: EventSink;
17
+ readonly clock: Clock;
18
+ /** TEAMS_CLAW_EVENTS_GONE: an event for a person with no chat here is answered 410 (else 200 "dropped", for an older Claw). */
19
+ readonly gone: boolean;
20
+ readonly metrics?: Pick<MetricsPort, "eventDelivered">;
21
+ readonly logger?: Logger;
22
+ }
23
+ /** `sha256=` HMAC of `<timestamp>.<body>` under the subscription's secret: Claw's signature. */
24
+ export declare function sign(secret: string, timestamp: string, body: Uint8Array): string;
25
+ /** Seconds from when Claw says the event happened (ISO 8601 with a zone) to `nowSeconds`; null when it does not say. */
26
+ export declare function sinceOccurred(occurredAt: unknown, nowSeconds: number): number | null;
27
+ export declare function clawEventsRoute(deps: ClawEventsDeps): HttpRoute;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The account-linking endpoint, reached from the console on the same origin: the console page creates an API key for the
3
+ * person and posts `{key}` here; the worker answers with a short code that the person types as `/link CODE` in the chat.
4
+ * The code only ever travels from the console to the chat, so a chat identity can never be tied to a key by a link someone
5
+ * else sent: anything but `key` in the body is ignored, and nothing is bound here.
6
+ */
7
+ import { type LinkRepository } from "@primusclaw/personal-agent";
8
+ import { type HttpRoute } from "./routes.js";
9
+ /** Both routes of the link flow live under this prefix, so one ingress path exposes them. */
10
+ export declare const PREFIX = "/primus-claw-agent";
11
+ /** Whether Claw takes `key` (a key Claw refuses gets no code). */
12
+ export type KeyCheck = (key: string) => Promise<boolean>;
13
+ export declare function linkRoute(links: Pick<LinkRepository, "stashPending">, checkKey: KeyCheck): HttpRoute;
14
+ export declare const healthzRoute: HttpRoute;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The worker's HTTP routes, independent of the server library: a route sees the request as read (headers, path
3
+ * parameters, query, the raw body) and answers with a status and a JSON or text body. The server (`server.ts`) mounts
4
+ * them; tests call them directly.
5
+ */
6
+ import { type JsonValue } from "@primusclaw/personal-agent";
7
+ export type Method = "GET" | "POST" | "PUT" | "DELETE";
8
+ export interface HttpRequest {
9
+ /** Header names lower case. */
10
+ readonly headers: Readonly<Record<string, string>>;
11
+ /** The path parameters (`:name` in the route's path), decoded. */
12
+ readonly params: Readonly<Record<string, string>>;
13
+ readonly query: URLSearchParams;
14
+ /** The body exactly as it arrived (an HMAC is computed over these bytes). */
15
+ readonly body: Uint8Array;
16
+ }
17
+ export type HttpResponse = {
18
+ readonly status: number;
19
+ readonly json: JsonValue;
20
+ } | {
21
+ readonly status: number;
22
+ readonly text: string;
23
+ readonly contentType?: string;
24
+ };
25
+ export interface HttpRoute {
26
+ readonly method: Method;
27
+ /** The path, with `:name` for a parameter (one segment). */
28
+ readonly path: string;
29
+ handle(request: HttpRequest): Promise<HttpResponse>;
30
+ }
31
+ export declare function json(status: number, body: JsonValue): HttpResponse;
32
+ /** The body as JSON; undefined when it is not JSON (or not UTF-8). */
33
+ export declare function bodyJson(request: HttpRequest): unknown;
34
+ /** The bearer token of the Authorization header ("" when there is none). */
35
+ export declare function bearerOf(request: HttpRequest): string;
36
+ /** Python's `str(value)` of a JSON value: a string as it is, anything else its repr (`None`, `True`, `{'a': 1}`). */
37
+ export declare function pythonStr(value: unknown): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The worker's HTTP server (fastify): the routes it is given, each body read raw (never parsed by the server: a route
3
+ * that verifies a signature needs the bytes as sent), within a size limit. With an access log, one line per request: its
4
+ * method, the route it matched as declared (`/claw-events/:route`, never the path as sent, nor its query or body: a path
5
+ * parameter or a query can carry a token), the status answered and how long it took (the Python worker's `aiohttp.access`).
6
+ */
7
+ import type { Logger } from "@primusclaw/personal-agent";
8
+ import { type FastifyInstance } from "fastify";
9
+ import type { HttpRoute } from "./routes.js";
10
+ export interface ServerOptions {
11
+ readonly routes: readonly HttpRoute[];
12
+ /** The largest body accepted, in bytes (413 beyond it). */
13
+ readonly maxBodyBytes: number;
14
+ /** Where the one line per request goes; none: no line. */
15
+ readonly accessLog?: Logger;
16
+ /** Routes (as declared) whose line is at debug, not info: a probe that answers every few seconds. */
17
+ readonly quietRoutes?: readonly string[];
18
+ }
19
+ /** The route a request matched, as declared; a request that matched none is logged as this, never by its path. */
20
+ export declare const UNMATCHED_ROUTE = "(unmatched)";
21
+ /** A server with `routes` mounted; not yet listening. */
22
+ export declare function buildServer(options: ServerOptions): FastifyInstance;
23
+ /** Listen on `host:port`; the port actually bound (for port 0). */
24
+ export declare function listen(app: FastifyInstance, host: string, port: number): Promise<number>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @primusclaw/personal-agent-runtime: Node implementations of core's ports. It depends on core only (never on a
3
+ * channel package); a channel package and a deployment's assembly wire these in.
4
+ */
5
+ export * from "./crypto-random.js";
6
+ export * from "./line-logger.js";
7
+ export * from "./system-clock.js";
8
+ export * from "./context/als-request-scope.js";
9
+ export * from "./claw/index.js";
10
+ export * from "./metrics/index.js";
11
+ export * from "./exchange/index.js";
12
+ export * from "./config/index.js";
13
+ export * from "./crypto/fernet.js";
14
+ export * from "./crypto/link-code.js";
15
+ export * from "./crypto/node-crypto.js";
16
+ export * from "./crypto/py-base64.js";
17
+ export * from "./sqlite/context.js";
18
+ export * from "./sqlite/reencrypt.js";
19
+ export * from "./sqlite/store.js";
20
+ export * from "./sqlite/open.js";
21
+ export * from "./http/routes.js";
22
+ export * from "./http/server.js";
23
+ export * from "./http/link.js";
24
+ export * from "./http/claw-events.js";
25
+ export * from "./github/index.js";
26
+ export * from "./github/feedback-config.js";
27
+ export * from "./github/feedback-issues.js";
28
+ export * from "./github/feedback-worker.js";
29
+ export * from "./logging/chat-log.js";
@@ -0,0 +1,15 @@
1
+ import { type LogFields, type Logger, type LogLevel } from "@primusclaw/personal-agent";
2
+ export interface LineLoggerOptions {
3
+ /** Lines below this level are dropped. */
4
+ readonly level: LogLevel;
5
+ /** Where a finished line goes (the process's stderr in production). */
6
+ readonly write: (line: string) => void;
7
+ /** The wall clock, for the timestamp. */
8
+ readonly now: () => number;
9
+ }
10
+ /**
11
+ * A logger that writes one line per call: `<ISO time> <LEVEL> <message> key=value ...`. The fields of `child` loggers
12
+ * come first, in the order they were bound. The whole line goes through `redactLogLine`: a net under the rule that a
13
+ * line carries ids, statuses and kinds of error only (credential shapes and credential-named URL parameters masked).
14
+ */
15
+ export declare function lineLogger(options: LineLoggerOptions, bound?: LogFields): Logger;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The worker's last log lines, tagged with the chat they were written for (the Python worker's `feedback.LogBuffer`
3
+ * and `LOG_SCOPE`): a report's scene shows the reporter's chat's last lines. The router runs each inbound event in the
4
+ * request scope; every line logged while it is handled (also by what it starts) is kept with that request's user and
5
+ * conversation, rebuilt from its sentence and its safe fields only (`safeLogText`). A line logged outside any request
6
+ * is not kept, as Python's `LogBuffer.emit` keeps none while `LOG_SCOPE` is None.
7
+ */
8
+ import { type Logger, type LogTail, type Mask, type RequestScope } from "@primusclaw/personal-agent";
9
+ export declare class ChatLog implements LogTail {
10
+ /** Whose message the current task is handling (the router enters it once per inbound event). */
11
+ private readonly scope;
12
+ private readonly mask;
13
+ /** Seconds since the epoch. */
14
+ private readonly now;
15
+ private readonly capacity;
16
+ private readonly lines;
17
+ constructor(
18
+ /** Whose message the current task is handling (the router enters it once per inbound event). */
19
+ scope: RequestScope, mask: Mask,
20
+ /** Seconds since the epoch. */
21
+ now: () => number, capacity?: number);
22
+ /** `inner`, also keeping each line logged in a chat's scope; `logger` names the part of the worker. */
23
+ logger(inner: Logger, logger: string): Logger;
24
+ /** The last lines kept for one chat, oldest first, as the scene shows them. */
25
+ tail(userId: string, conversationId: string, count?: number): readonly string[];
26
+ }
@@ -0,0 +1,17 @@
1
+ /** Numbers and histograms in the Prometheus text format (version 0.0.4), written as the Python worker wrote them. */
2
+ /** Python's `round(value, 3)`: a tie on the exact binary value goes to the even neighbour. */
3
+ export declare function round3(value: number): number;
4
+ /** Python's `repr(float(value))`: the shortest digits, in exponent form below 1e-4 and from 1e16 on. */
5
+ export declare function pyFloatRepr(value: number): string;
6
+ /** A sample's value: a whole number as an integer, anything else as Python's float repr. */
7
+ export declare function formatNumber(value: number): string;
8
+ /** Bucket counts of one histogram series: cumulative buckets, the sum and the count. */
9
+ export declare class Histogram {
10
+ private readonly buckets;
11
+ private readonly counts;
12
+ private total;
13
+ private count;
14
+ constructor(buckets: readonly number[]);
15
+ observe(value: number): void;
16
+ lines(name: string, labels: string): string[];
17
+ }
@@ -0,0 +1,2 @@
1
+ export { formatNumber, pyFloatRepr, round3 } from "./exposition.js";
2
+ export { ALERTED, CONTENT_TYPE, EVENT_BUCKETS, HELP, LAG_WINDOW_MS, PrometheusMetrics } from "./prometheus-metrics.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The worker's counters for the operator's alerts, in memory (they start from zero when the worker restarts), written in
3
+ * the Prometheus text format for `GET /metrics` on its own port (the listener is the channel's assembly). The text is
4
+ * the Python worker's, line for line: the alert queries and dashboards read these names and labels.
5
+ */
6
+ import { type Clock, type MetricsPort, type SecurityEventRecord } from "@primusclaw/personal-agent";
7
+ export declare const CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
8
+ /** `clawy_inbound_lag_max_ms`: the longest wait of the messages handled in this many milliseconds. */
9
+ export declare const LAG_WINDOW_MS = 60000;
10
+ /** Seconds; the 5 s bucket is the "card within 5 s" line. */
11
+ export declare const EVENT_BUCKETS: readonly [0.5, 1, 2, 3, 5, 10, 30, 60];
12
+ type Labels = ReadonlyArray<readonly [string, string]>;
13
+ /** Series written as 0 from the start, so that each one an alert reads exists before its first event. */
14
+ export declare const ALERTED: ReadonlyArray<readonly [string, Labels]>;
15
+ /** Every metric: its type and help text. */
16
+ export declare const HELP: Readonly<Record<string, readonly [string, string]>>;
17
+ /** The MetricsPort of a worker, kept in memory and written in the Prometheus text format. */
18
+ export declare class PrometheusMetrics implements MetricsPort {
19
+ private readonly clock;
20
+ private readonly lagWindowMs;
21
+ private readonly events;
22
+ private handledCount;
23
+ private failedCount;
24
+ private handleMsTotal;
25
+ private readonly limits;
26
+ private readonly dropped;
27
+ /** (when, lag): decreasing lags, the sliding-window maximum. */
28
+ private readonly lags;
29
+ /** Event type -> (receipt to sent, occurred to sent), each written from the start. */
30
+ private readonly eventTimes;
31
+ constructor(clock: Clock, lagWindowMs?: number);
32
+ /** Inbound messages handled so far. */
33
+ get handled(): number;
34
+ eventDelivered(eventType: string, handleSeconds: number, sinceOccurredSeconds: number | null): void;
35
+ securityEvent(record: SecurityEventRecord): void;
36
+ processed(lagMs: number | null, handleMs: number, ok: boolean): void;
37
+ limited(reason: string): void;
38
+ filesDropped(path: string, count?: number): void;
39
+ /** The longest wait of the messages handled in the window, in milliseconds (0: none). */
40
+ lagMaxMs(): number;
41
+ private expire;
42
+ /** All series in the Prometheus text exposition format (version 0.0.4). */
43
+ render(): string;
44
+ private renderHistograms;
45
+ }
46
+ export {};