@blogic-cz/agent-tools 1.10.1 → 1.11.1

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/README.md CHANGED
@@ -547,6 +547,8 @@ Each tool uses its own auth method — no unified token store:
547
547
  | `api-tool` | Caller capability and proxy HTTPS origin in client configuration |
548
548
  | `api-proxy` | Private operator store and policy under a separated service identity |
549
549
 
550
+ Set `tokenEnvVar` and `passwordEnvVar` to names that match `^[A-Z_][A-Z0-9_]*$`.
551
+
550
552
  The REST proxy uses its separate private operator store described in [REST proxy setup](docs/rest-api-proxy.md). Other tool configs reference credential sources. Provider secrets are **never** stored in the caller client config file. The `db-tool` config references env var **names** only:
551
553
 
552
554
  ```json5
@@ -574,7 +576,8 @@ These are **overrides with fallback**, not requirements — the opposite of `pas
574
576
  - variable unset or empty → the literal `host` / `port` in the config is used, and no error is raised;
575
577
  - `portEnvVar` set to something that is not an integer between 1 and 65535 → the command **fails**. It never falls back to the literal, because that would query a different database while you believe the override applied.
576
578
 
577
- The variable is read from the process environment first (Bun loads `.env` files into it), then from `~/.zshrc` exports.
579
+ The variable is read from the process environment first (Bun loads `.env` files into it), then from the interactive zsh environment.
580
+ The zsh child receives only `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TERM`, `LANG`, `LC_ALL`, `TMPDIR`, `XDG_CONFIG_HOME`, and `ZDOTDIR`. If an rc file needs another parent process variable to select or load a value, that value will not resolve.
578
581
 
579
582
  The typical use is a repository where every git worktree runs its own database container on its own host port, published by that worktree's `.env`:
580
583
 
@@ -2,6 +2,7 @@ import { ChildProcessSpawner } from "effect/process";
2
2
  import { Context, Effect, Layer } from "effect";
3
3
  import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./types";
4
4
  import { ConfigService } from "#config";
5
+ import { SecretEnvResolver } from "#shared/secret-env";
5
6
  import { DbConfigService } from "./config-service";
6
7
  import { DbSqlClient } from "./sql-client";
7
8
  import { type DbError } from "./errors";
@@ -36,6 +37,6 @@ declare const DbService_base: Context.ServiceClass<DbService, "@agent-tools/DbSe
36
37
  readonly executeSchemaQuery: (env: string, mode: SchemaMode, table?: string) => Effect.Effect<QueryResult, DbError>;
37
38
  }>;
38
39
  export declare class DbService extends DbService_base {
39
- static readonly layer: Layer.Layer<DbService, never, ChildProcessSpawner.ChildProcessSpawner | ConfigService | DbConfigService | DbSqlClient>;
40
+ static readonly layer: Layer.Layer<DbService, never, ChildProcessSpawner.ChildProcessSpawner | ConfigService | DbConfigService | DbSqlClient | SecretEnvResolver>;
40
41
  }
41
42
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/db-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAS,OAAO,EAAY,MAAM,EAAE,KAAK,EAAuB,MAAM,QAAQ,CAAC;AAEtF,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtF,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAQxC,OAAO,EAAE,eAAe,EAA4B,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAqB,MAAM,cAAc,CAAC;AAC9D,OAAO,EAKL,KAAK,OAAO,EACb,MAAM,UAAU,CAAC;AA8BlB,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,oBAAoB,CAqBtB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,MAAM,CAOR;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,OAAO,EACzB,gBAAgB,GAAE,SAAS,mBAAmB,EAAO,GACpD,IAAI,CAAC,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,MAAM,GAAG,aAAa,CAAC,CAUhF;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,wBAAwB,CAAC,GACvF,OAAO,CAMT;AAGD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;;2BAK3B,CACrB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC;iCACX,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,UAAU,EAChB,KAAK,CAAC,EAAE,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC;;AAZ5C,qBAAa,SAAU,SAAQ,cAcF;IAC3B,MAAM,CAAC,QAAQ,CAAC,KAAK,yHAgwBnB;CACH"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/db-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAS,OAAO,EAAY,MAAM,EAAE,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAEjF,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtF,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAIxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAKvD,OAAO,EAAE,eAAe,EAA4B,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAqB,MAAM,cAAc,CAAC;AAC9D,OAAO,EAKL,KAAK,OAAO,EACb,MAAM,UAAU,CAAC;AAwBlB,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,oBAAoB,CAqBtB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,MAAM,CAOR;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,OAAO,EACzB,gBAAgB,GAAE,SAAS,mBAAmB,EAAO,GACpD,IAAI,CAAC,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,MAAM,GAAG,aAAa,CAAC,CAUhF;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,wBAAwB,CAAC,GACvF,OAAO,CAMT;AAGD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;;2BAK3B,CACrB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC;iCACX,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,UAAU,EAChB,KAAK,CAAC,EAAE,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC;;AAZ5C,qBAAa,SAAU,SAAQ,cAcF;IAC3B,MAAM,CAAC,QAAQ,CAAC,KAAK,6IAmvBnB;CACH"}
@@ -14,6 +14,7 @@ export type DbQueryOutcome = {
14
14
  readonly command: string;
15
15
  };
16
16
  export declare const toErrorMessage: (cause: unknown) => string;
17
+ export declare const toDbQueryError: (cause: unknown, sql: string) => DbQueryError;
17
18
  declare const DbSqlClient_base: Context.ServiceClass<DbSqlClient, "@agent-tools/DbSqlClient", {
18
19
  readonly run: (connection: DbConnection, sql: string) => Effect.Effect<DbQueryOutcome, DbQueryError>;
19
20
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"sql-client.d.ts","sourceRoot":"","sources":["../../src/db-tool/sql-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAIF,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,MACQ,CAAC;;kBAkDvC,CACZ,UAAU,EAAE,YAAY,EACxB,GAAG,EAAE,MAAM,KACR,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,YAAY,CAAC;;AANpD,qBAAa,WAAY,SAAQ,gBAQF;IAC7B,MAAM,CAAC,QAAQ,CAAC,KAAK,yCAAsD;CAC5E"}
1
+ {"version":3,"file":"sql-client.d.ts","sourceRoot":"","sources":["../../src/db-tool/sql-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAIF,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,MACQ,CAAC;AAEzD,eAAO,MAAM,cAAc,UAAW,OAAO,OAAO,MAAM,KAAG,YAG5D,CAAC;;kBA+CgB,CACZ,UAAU,EAAE,YAAY,EACxB,GAAG,EAAE,MAAM,KACR,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,YAAY,CAAC;;AANpD,qBAAa,WAAY,SAAQ,gBAQF;IAC7B,MAAM,CAAC,QAAQ,CAAC,KAAK,yCAAsD;CAC5E"}
@@ -18,5 +18,5 @@ export declare function extractLogsFromDsQuery(response: {
18
18
  };
19
19
  };
20
20
  }): LogLine[];
21
- export declare const logsCommand: Command.Command<"logs", {} | {}, {}, never, import("../config").ConfigService>;
21
+ export declare const logsCommand: Command.Command<"logs", {} | {}, {}, never, import("../config").ConfigService | import("../shared/secret-env").SecretEnvResolver>;
22
22
  //# sourceMappingURL=logs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../../src/observability-tool/logs.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,OAAO,EAAQ,MAAM,YAAY,CAAC;AAarD,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AA6C1D,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE;IAC/C,OAAO,EAAE;QACP,CAAC,EAAE;YACD,MAAM,CAAC,EAAE,KAAK,CAAC;gBACb,MAAM,EAAE;oBAAE,MAAM,EAAE,KAAK,CAAC;wBAAE,IAAI,EAAE,MAAM,CAAC;wBAAC,IAAI,CAAC,EAAE,MAAM,CAAC;wBAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;qBAAE,CAAC,CAAA;iBAAE,CAAC;gBAC5F,IAAI,EAAE;oBAAE,MAAM,EAAE,OAAO,EAAE,EAAE,CAAA;iBAAE,CAAC;aAC/B,CAAC,CAAC;SACJ,CAAC;KACH,CAAC;CACH,GAAG,OAAO,EAAE,CAiCZ;AA4ED,eAAO,MAAM,WAAW,gFAGvB,CAAC"}
1
+ {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../../src/observability-tool/logs.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,OAAO,EAAQ,MAAM,YAAY,CAAC;AAarD,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AA6C1D,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE;IAC/C,OAAO,EAAE;QACP,CAAC,EAAE;YACD,MAAM,CAAC,EAAE,KAAK,CAAC;gBACb,MAAM,EAAE;oBAAE,MAAM,EAAE,KAAK,CAAC;wBAAE,IAAI,EAAE,MAAM,CAAC;wBAAC,IAAI,CAAC,EAAE,MAAM,CAAC;wBAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;qBAAE,CAAC,CAAA;iBAAE,CAAC;gBAC5F,IAAI,EAAE;oBAAE,MAAM,EAAE,OAAO,EAAE,EAAE,CAAA;iBAAE,CAAC;aAC/B,CAAC,CAAC;SACJ,CAAC;KACH,CAAC;CACH,GAAG,OAAO,EAAE,CAiCZ;AA4ED,eAAO,MAAM,WAAW,mIAGvB,CAAC"}
@@ -1,3 +1,3 @@
1
1
  import { Command } from "effect/cli";
2
- export declare const metricsCommand: Command.Command<"metrics", {} | {}, {}, never, import("../config").ConfigService>;
2
+ export declare const metricsCommand: Command.Command<"metrics", {} | {}, {}, never, import("../config").ConfigService | import("../shared/secret-env").SecretEnvResolver>;
3
3
  //# sourceMappingURL=metrics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/observability-tool/metrics.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,OAAO,EAAQ,MAAM,YAAY,CAAC;AA4HrD,eAAO,MAAM,cAAc,mFAG1B,CAAC"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/observability-tool/metrics.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,OAAO,EAAQ,MAAM,YAAY,CAAC;AA4HrD,eAAO,MAAM,cAAc,sIAG1B,CAAC"}
@@ -2,6 +2,7 @@ import { Effect, Option } from "effect";
2
2
  import { Flag } from "effect/cli";
3
3
  import { ConfigService } from "#config";
4
4
  import { ObservabilityToolError } from "./errors";
5
+ import { SecretEnvResolver } from "#shared/secret-env";
5
6
  import type { DsQueryOpts, DsQueryResponse, ObservabilityEnvConfig } from "./types";
6
7
  export declare function formatObservabilityError(error: unknown): string;
7
8
  export declare const envOption: Flag.Flag<string>;
@@ -11,7 +12,7 @@ export declare const profileOption: Flag.Flag<Option.Option<string>>;
11
12
  * building a request against an undefined datasource UID.
12
13
  */
13
14
  export declare function requireTempoUid(config: ObservabilityEnvConfig): Effect.Effect<string, ObservabilityToolError>;
14
- export declare const resolveConfig: (env: string, profile: Option.Option<string>) => Effect.Effect<ObservabilityEnvConfig, ObservabilityToolError, ConfigService>;
15
+ export declare const resolveConfig: (env: string, profile: Option.Option<string>) => Effect.Effect<ObservabilityEnvConfig, ObservabilityToolError, ConfigService | SecretEnvResolver>;
15
16
  export declare function buildHeaders(token?: string): Headers;
16
17
  export declare function observabilityFetch<T>(config: ObservabilityEnvConfig, path: string, init?: RequestInit): Effect.Effect<T, ObservabilityToolError>;
17
18
  export declare function observabilityDsQuery(config: ObservabilityEnvConfig, datasourceUid: string, datasourceType: "prometheus" | "loki", expr: string, options?: DsQueryOpts): Effect.Effect<DsQueryResponse, ObservabilityToolError>;
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/observability-tool/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,aAAa,EAAiB,MAAM,SAAS,CAAC;AAGvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAElD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EAEf,sBAAsB,EACvB,MAAM,SAAS,CAAC;AAOjB,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAe/D;AAED,eAAO,MAAM,SAAS,mBAGrB,CAAC;AAEF,eAAO,MAAM,aAAa,kCAMzB,CAAC;AAmCF;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAU/C;AAyDD,eAAO,MAAM,aAAa,QAAS,MAAM,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,iFA4BrE,CAAC;AAEL,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAWpD;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,WAAW,GACjB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAgE1C;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,sBAAsB,EAC9B,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,YAAY,GAAG,MAAM,EACrC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,WAAW,GACpB,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAkCxD"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/observability-tool/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,aAAa,EAAiB,MAAM,SAAS,CAAC;AAGvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EAEf,sBAAsB,EACvB,MAAM,SAAS,CAAC;AAOjB,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAe/D;AAED,eAAO,MAAM,SAAS,mBAGrB,CAAC;AAEF,eAAO,MAAM,aAAa,kCAMzB,CAAC;AAkCF;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAU/C;AA0DD,eAAO,MAAM,aAAa,QAAS,MAAM,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,qGAiCrE,CAAC;AAEL,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAWpD;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,WAAW,GACjB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAgE1C;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,sBAAsB,EAC9B,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,YAAY,GAAG,MAAM,EACrC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,WAAW,GACpB,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAkCxD"}
@@ -4,5 +4,5 @@ import { ObservabilityToolError } from "./errors";
4
4
  import type { ObservabilityEnvConfig, SearchWindow, TempoSearchResponse, TraceSearchHit } from "./types";
5
5
  export declare function summarizeSearchHits(response: TempoSearchResponse): TraceSearchHit[];
6
6
  export declare function searchTempoByQuery(config: ObservabilityEnvConfig, query: string, window: SearchWindow, limit: number): Effect.Effect<TempoSearchResponse, ObservabilityToolError>;
7
- export declare const traceCommand: Command.Command<"trace", {} | {}, {}, never, import("../config").ConfigService>;
7
+ export declare const traceCommand: Command.Command<"trace", {} | {}, {}, never, import("../config").ConfigService | import("../shared/secret-env").SecretEnvResolver>;
8
8
  //# sourceMappingURL=trace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/observability-tool/trace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAe,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAAY,OAAO,EAAQ,MAAM,YAAY,CAAC;AAKrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAWlD,OAAO,KAAK,EAEV,sBAAsB,EAItB,YAAY,EAEZ,mBAAmB,EAEnB,cAAc,EAEf,MAAM,SAAS,CAAC;AAuVjB,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,cAAc,EAAE,CAsBnF;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,sBAAsB,EAC9B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,CA4C5D;AAoPD,eAAO,MAAM,YAAY,iFAGxB,CAAC"}
1
+ {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/observability-tool/trace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAe,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAAY,OAAO,EAAQ,MAAM,YAAY,CAAC;AAKrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAWlD,OAAO,KAAK,EAEV,sBAAsB,EAItB,YAAY,EAEZ,mBAAmB,EAEnB,cAAc,EAEf,MAAM,SAAS,CAAC;AAuVjB,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,cAAc,EAAE,CAsBnF;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,sBAAsB,EAC9B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,CA4C5D;AAoPD,eAAO,MAAM,YAAY,oIAGxB,CAAC"}
@@ -1,5 +1,7 @@
1
1
  /** Redact a credential a tool resolved outside a sensitively named environment variable. */
2
- export declare const registerResolvedSecret: (value: string) => void;
2
+ export declare const registerResolvedSecret: (value: string, options?: {
3
+ readonly allowShort?: boolean;
4
+ }) => void;
3
5
  export declare function unsafeOutboundTextReason(text: string, environment?: Record<string, string | undefined>): string | null;
4
6
  /** Remove recognized secret spans and known sensitive process-environment values. */
5
7
  export declare function redactSensitiveText(text: string, environment?: Record<string, string | undefined>): string;
@@ -1 +1 @@
1
- {"version":3,"file":"content-security.d.ts","sourceRoot":"","sources":["../../src/shared/content-security.ts"],"names":[],"mappings":"AAaA,4FAA4F;AAC5F,eAAO,MAAM,sBAAsB,UAAW,MAAM,SAEnD,CAAC;AAUF,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAC5D,MAAM,GAAG,IAAI,CAcf;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAC5D,MAAM,CAuBR;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,EACR,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAC5D,CAAC,CAqBH"}
1
+ {"version":3,"file":"content-security.d.ts","sourceRoot":"","sources":["../../src/shared/content-security.ts"],"names":[],"mappings":"AAaA,4FAA4F;AAC5F,eAAO,MAAM,sBAAsB,UAC1B,MAAM,YACJ;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,SAK3C,CAAC;AAUF,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAC5D,MAAM,GAAG,IAAI,CAcf;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAC5D,MAAM,CAuBR;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,EACR,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAC5D,CAAC,CAqBH"}
@@ -0,0 +1,22 @@
1
+ import { Context, Layer } from "effect";
2
+ export type LookupResult = {
3
+ readonly _tag: "found";
4
+ readonly value: string;
5
+ } | {
6
+ readonly _tag: "absent";
7
+ } | {
8
+ readonly _tag: "failed";
9
+ readonly reason: string;
10
+ };
11
+ export type ZshRunner = (name: string, timeoutMs: number) => Promise<LookupResult>;
12
+ declare const SecretEnvResolver_base: Context.ServiceClass<SecretEnvResolver, "@agent-tools/SecretEnvResolver", {
13
+ readonly resolveSecretEnv: (name: string) => Promise<string>;
14
+ readonly lookupConfigEnv: (name: string) => Promise<LookupResult>;
15
+ }>;
16
+ export declare class SecretEnvResolver extends SecretEnvResolver_base {
17
+ static get liveLayer(): Layer.Layer<SecretEnvResolver, never, never>;
18
+ static readonly testLayer: (runner?: ZshRunner) => Layer.Layer<SecretEnvResolver, never, never>;
19
+ }
20
+ export declare function makeSecretEnvResolverLayer(runner?: ZshRunner, timeoutMs?: number): Layer.Layer<SecretEnvResolver, never, never>;
21
+ export {};
22
+ //# sourceMappingURL=secret-env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-env.d.ts","sourceRoot":"","sources":["../../src/shared/secret-env.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAqBxC,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;;+BAmJpD,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC;8BAClC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC;;AAJrE,qBAAa,iBAAkB,SAAQ,sBAMF;IACnC,MAAM,KAAK,SAAS,iDAEnB;IACD,MAAM,CAAC,QAAQ,CAAC,SAAS,YAAY,SAAS,kDACT;CACtC;AAED,wBAAgB,0BAA0B,CACxC,MAAM,GAAE,SAA6B,EACrC,SAAS,SAAqB,gDAwC/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "1.10.1",
3
+ "version": "1.11.1",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure platform, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -14,6 +14,7 @@ import {
14
14
  VERSION,
15
15
  } from "#shared";
16
16
  import { AuditServiceLayer, withAudit } from "#shared/audit";
17
+ import { SecretEnvResolver } from "#shared/secret-env";
17
18
  import { withRedactedOutput } from "#shared/output-boundary";
18
19
  import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#config";
19
20
  import { DbConfigService, makeDbConfigLayer } from "./config-service";
@@ -248,6 +249,7 @@ const MainLayer = DbService.layer.pipe(
248
249
  // provideMerge (not provide) so DbConfigService stays in the program context for the `envs` command,
249
250
  // which reads it directly rather than going through DbService.
250
251
  Layer.provideMerge(DbSqlClient.layer),
252
+ Layer.provideMerge(SecretEnvResolver.liveLayer),
251
253
  Layer.provideMerge(dbConfigLayer),
252
254
  Layer.provideMerge(ConfigServiceLayer),
253
255
  Layer.provideMerge(BunServices.layer),
@@ -1,5 +1,5 @@
1
1
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
2
- import { Clock, Context, Duration, Effect, Layer, Ref, Result, Stream } from "effect";
2
+ import { Clock, Context, Duration, Effect, Layer, Result, Stream } from "effect";
3
3
 
4
4
  import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./types";
5
5
 
@@ -7,6 +7,7 @@ import { ConfigService } from "#config";
7
7
  import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
8
8
  import { resolveEnvTemplate } from "#shared/env-template";
9
9
  import { registerResolvedSecret, unsafeOutboundTextReason } from "#shared/content-security";
10
+ import { SecretEnvResolver } from "#shared/secret-env";
10
11
  import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
11
12
  import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
12
13
  import { buildApiProbeArgs } from "#shared/k8s-probe";
@@ -43,12 +44,6 @@ const MIN_TCP_PORT = 1;
43
44
  const MAX_TCP_PORT = 65535;
44
45
  const INTEGER_ONLY = /^\d+$/;
45
46
 
46
- // Bun.env is the primary source: it carries the values Bun loads from a .env file, which is where
47
- // a per-worktree host/port lives. It aliases process.env, which is also the only one that exists
48
- // when this module runs outside Bun (the vitest runner).
49
- const readProcessEnv = (envVar: string): string | undefined =>
50
- (typeof Bun === "undefined" ? process.env : Bun.env)[envVar];
51
-
52
47
  export type ResolvedPortOverride =
53
48
  | { readonly ok: true; readonly port: number }
54
49
  | { readonly ok: false; readonly message: string };
@@ -159,6 +154,7 @@ export class DbService extends Context.Service<
159
154
  Effect.gen(function* () {
160
155
  const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
161
156
  const sqlClient = yield* DbSqlClient;
157
+ const secretEnvResolver = yield* SecretEnvResolver;
162
158
  const agentToolsConfig = yield* ConfigService;
163
159
  const dbConfig = yield* DbConfigService;
164
160
 
@@ -185,152 +181,138 @@ export class DbService extends Context.Service<
185
181
  const apiProbeTimeoutMs = dbConfig.apiProbeTimeoutMs ?? 2000;
186
182
  const remotePort = dbConfig.remotePort ?? 5432;
187
183
 
188
- const zshrcEnvCache = yield* Ref.make<Record<string, string> | null>(null);
189
184
  const envTemplateRegex = /^\$\{([A-Za-z0-9_]+)\}$/;
185
+ const lookupConfigEnv = (name: string) =>
186
+ Effect.tryPromise({
187
+ try: () => secretEnvResolver.lookupConfigEnv(name),
188
+ catch: () => new Error(`${name} lookup failed`),
189
+ });
190
190
 
191
- const loadEnvFromZshrc = Effect.fn("DbService.loadEnvFromZshrc")(function* () {
192
- const cached = yield* Ref.get(zshrcEnvCache);
193
- if (cached !== null) {
194
- return cached;
195
- }
196
-
197
- const home = process.env.HOME;
198
- if (!home || home.trim() === "") {
199
- yield* Ref.set(zshrcEnvCache, {});
200
- return {};
201
- }
202
-
203
- const zshrcPath = `${home}/.zshrc`;
204
- const content = yield* Effect.tryPromise(async () => {
205
- const file = Bun.file(zshrcPath);
206
- if (!(await file.exists())) {
207
- return "";
191
+ const resolvePassword = (config: DbConfig, env: string) =>
192
+ Effect.gen(function* () {
193
+ if (config.password) {
194
+ registerResolvedSecret(config.password);
195
+ return config.password;
208
196
  }
209
- return await file.text();
210
- }).pipe(Effect.orElseSucceed(() => ""));
211
197
 
212
- const envVars: Record<string, string> = {};
213
- const regex = /^export\s+([A-Z_][A-Z0-9_]*)=["']?([^"'\n]+)["']?/gm;
214
- let match = regex.exec(content);
215
-
216
- while (match !== null) {
217
- envVars[match[1]] = match[2];
218
- match = regex.exec(content);
219
- }
220
-
221
- yield* Ref.set(zshrcEnvCache, envVars);
222
- return envVars;
223
- });
198
+ const passwordEnvVar = config.passwordEnvVar;
199
+ if (passwordEnvVar) {
200
+ return yield* Effect.tryPromise({
201
+ try: () => secretEnvResolver.resolveSecretEnv(passwordEnvVar),
202
+ catch: () =>
203
+ new Error(
204
+ `${passwordEnvVar} is not in the environment or in the interactive zsh`,
205
+ ),
206
+ }).pipe(
207
+ Effect.mapError(
208
+ () =>
209
+ new DbConnectionError({
210
+ message: `${passwordEnvVar} is not in the environment or in the interactive zsh`,
211
+ environment: env,
212
+ }),
213
+ ),
214
+ );
215
+ }
224
216
 
225
- const resolvePassword = Effect.fn("DbService.resolvePassword")(function* (
226
- config: DbConfig,
227
- env: string,
228
- ) {
229
- if (config.password) {
230
- registerResolvedSecret(config.password);
231
- return config.password;
232
- }
217
+ // Local databases typically don't need a password
218
+ return "";
219
+ });
233
220
 
234
- if (config.passwordEnvVar) {
235
- const fromEnv = process.env[config.passwordEnvVar];
236
- if (fromEnv) {
237
- registerResolvedSecret(fromEnv);
238
- return fromEnv;
221
+ const resolveConfigString = (value: string, env: string, label: string) =>
222
+ Effect.gen(function* () {
223
+ const match = value.match(envTemplateRegex);
224
+ if (!match) return value;
225
+
226
+ const envVar = match[1];
227
+ const result = yield* lookupConfigEnv(envVar).pipe(
228
+ Effect.mapError(
229
+ () =>
230
+ new DbConnectionError({
231
+ message: `${envVar} lookup failed (required for '${label}' config field) in environment ${env}.`,
232
+ environment: env,
233
+ }),
234
+ ),
235
+ );
236
+ if (result._tag === "failed") {
237
+ return yield* new DbConnectionError({
238
+ message: `${envVar} lookup failed (required for '${label}' config field) in environment ${env}.`,
239
+ environment: env,
240
+ });
239
241
  }
240
-
241
- const zshrcEnv = yield* loadEnvFromZshrc();
242
- const fromZsh = zshrcEnv[config.passwordEnvVar];
243
- if (fromZsh) {
244
- registerResolvedSecret(fromZsh);
245
- return fromZsh;
242
+ if (result._tag === "absent") {
243
+ return yield* new DbConnectionError({
244
+ message: `${envVar} is not in the environment or in the interactive zsh (required for '${label}' config field) in environment ${env}.`,
245
+ environment: env,
246
+ });
246
247
  }
247
-
248
- return yield* new DbConnectionError({
249
- message: `Environment variable ${config.passwordEnvVar} is not set.`,
250
- environment: env,
251
- });
252
- }
253
-
254
- // Local databases typically don't need a password
255
- return "";
256
- });
257
-
258
- const resolveConfigString = Effect.fn("DbService.resolveConfigString")(function* (
259
- value: string,
260
- env: string,
261
- label: string,
262
- zshrcEnv: Record<string, string>,
263
- ) {
264
- const match = value.match(envTemplateRegex);
265
- if (!match) return value;
266
-
267
- const envVar = match[1];
268
- const fromEnv = Bun.env[envVar];
269
- if (fromEnv !== undefined) return fromEnv;
270
-
271
- const fromZsh = zshrcEnv[envVar];
272
- if (fromZsh !== undefined) return fromZsh;
273
-
274
- return yield* new DbConnectionError({
275
- message: `Environment variable ${envVar} (required for '${label}' config field) is not set in environment ${env}.`,
276
- environment: env,
248
+ return result.value;
277
249
  });
278
- });
279
250
 
280
- const lookupEnvOverride = (envVar: string, zshrcEnv: Record<string, string>) =>
281
- readProcessEnv(envVar) ?? zshrcEnv[envVar];
282
-
283
- const resolveDbConfig = Effect.fn("DbService.resolveDbConfig")(function* (
284
- config: DbConfig,
285
- env: string,
286
- ) {
287
- const needsEnvResolution =
288
- envTemplateRegex.test(config.user) ||
289
- envTemplateRegex.test(config.database) ||
290
- config.hostEnvVar !== undefined ||
291
- config.portEnvVar !== undefined;
292
- const zshrcEnv = needsEnvResolution ? yield* loadEnvFromZshrc() : {};
293
-
294
- const host = resolveHostOverride(
295
- config.host,
296
- config.hostEnvVar,
297
- config.hostEnvVar ? lookupEnvOverride(config.hostEnvVar, zshrcEnv) : undefined,
298
- );
299
- const portOverride = resolvePortOverride(
300
- env,
301
- config.port,
302
- config.portEnvVar,
303
- config.portEnvVar ? lookupEnvOverride(config.portEnvVar, zshrcEnv) : undefined,
304
- );
251
+ const resolveDbConfig = (config: DbConfig, env: string) =>
252
+ Effect.gen(function* () {
253
+ const resolveOverride = (name: string | undefined) => {
254
+ if (!name) return Effect.succeed(undefined);
255
+ return lookupConfigEnv(name).pipe(
256
+ Effect.flatMap((result) => {
257
+ if (result._tag === "absent") return Effect.succeed(undefined);
258
+ if (result._tag === "failed") {
259
+ return Effect.fail(
260
+ new DbConnectionError({
261
+ message: `${name} lookup failed in environment ${env}.`,
262
+ environment: env,
263
+ }),
264
+ );
265
+ }
266
+ return Effect.succeed(result.value);
267
+ }),
268
+ Effect.mapError(
269
+ () =>
270
+ new DbConnectionError({
271
+ message: `${name} lookup failed in environment ${env}.`,
272
+ environment: env,
273
+ }),
274
+ ),
275
+ );
276
+ };
277
+ const hostValue = yield* resolveOverride(config.hostEnvVar);
278
+ const portValue = yield* resolveOverride(config.portEnvVar);
279
+
280
+ const host = resolveHostOverride(config.host, config.hostEnvVar, hostValue);
281
+ const portOverride = resolvePortOverride(
282
+ env,
283
+ config.port,
284
+ config.portEnvVar,
285
+ portValue,
286
+ );
305
287
 
306
- if (!portOverride.ok) {
307
- return yield* new DbConnectionError({
308
- message: portOverride.message,
309
- environment: env,
310
- });
311
- }
288
+ if (!portOverride.ok) {
289
+ return yield* new DbConnectionError({
290
+ message: portOverride.message,
291
+ environment: env,
292
+ });
293
+ }
312
294
 
313
- // A host override can flip the tunnel decision (a forwarded localhost port vs a directly
314
- // reachable host), so re-derive the access mode whenever the override changed the host.
315
- const needsTunnel =
316
- host === config.host
317
- ? config.needsTunnel
318
- : resolveDbAccessMode(
319
- env,
320
- host,
321
- dbConfig.kubectl !== undefined,
322
- config.allowedMutations,
323
- ).needsTunnel;
295
+ // A host override can flip the tunnel decision (a forwarded localhost port vs a directly
296
+ // reachable host), so re-derive the access mode whenever the override changed the host.
297
+ const needsTunnel =
298
+ host === config.host
299
+ ? config.needsTunnel
300
+ : resolveDbAccessMode(
301
+ env,
302
+ host,
303
+ dbConfig.kubectl !== undefined,
304
+ config.allowedMutations,
305
+ ).needsTunnel;
324
306
 
325
- return {
326
- ...config,
327
- host,
328
- port: portOverride.port,
329
- needsTunnel,
330
- user: yield* resolveConfigString(config.user, env, "user", zshrcEnv),
331
- database: yield* resolveConfigString(config.database, env, "database", zshrcEnv),
332
- };
333
- });
307
+ return {
308
+ ...config,
309
+ host,
310
+ port: portOverride.port,
311
+ needsTunnel,
312
+ user: yield* resolveConfigString(config.user, env, "user"),
313
+ database: yield* resolveConfigString(config.database, env, "database"),
314
+ };
315
+ });
334
316
 
335
317
  const executeShellCommand = (command: ChildProcess.Command) =>
336
318
  Effect.scoped(
@@ -1,6 +1,7 @@
1
1
  import { Context, Effect, Layer } from "effect";
2
2
 
3
3
  import { DbQueryError } from "./errors";
4
+ import { redactSensitiveText } from "#shared/content-security";
4
5
 
5
6
  export type DbConnection = {
6
7
  readonly host: string;
@@ -22,6 +23,11 @@ type BunQueryResult = unknown[] & { count?: number; command?: string };
22
23
  export const toErrorMessage = (cause: unknown): string =>
23
24
  cause instanceof Error ? cause.message : String(cause);
24
25
 
26
+ export const toDbQueryError = (cause: unknown, sql: string): DbQueryError => {
27
+ const message = redactSensitiveText(toErrorMessage(cause));
28
+ return new DbQueryError({ message, sql, stderr: message });
29
+ };
30
+
25
31
  const runWithBunSql = (connection: DbConnection, sql: string) =>
26
32
  Effect.tryPromise({
27
33
  try: async (): Promise<DbQueryOutcome> => {
@@ -61,10 +67,7 @@ const runWithBunSql = (connection: DbConnection, sql: string) =>
61
67
  await client.close().catch(() => undefined);
62
68
  }
63
69
  },
64
- catch: (cause) => {
65
- const message = toErrorMessage(cause);
66
- return new DbQueryError({ message, sql, stderr: message });
67
- },
70
+ catch: (cause) => toDbQueryError(cause, sql),
68
71
  });
69
72
 
70
73
  export class DbSqlClient extends Context.Service<
@@ -7,6 +7,7 @@ import { Command } from "effect/cli";
7
7
  import { ConfigServiceLayer } from "#config";
8
8
  import { AuditServiceLayer, withAudit } from "#shared/audit";
9
9
  import { withRedactedOutput } from "#shared/output-boundary";
10
+ import { SecretEnvResolver } from "#shared/secret-env";
10
11
  import { makeSchemaCommand, renderCauseToStderr, VERSION } from "#shared";
11
12
 
12
13
  import { metricsCommand } from "./metrics";
@@ -24,7 +25,12 @@ const mainCommand = Command.make("observability-tool", {}).pipe(
24
25
 
25
26
  const cli = Command.run(mainCommand, { version: VERSION, renderErrors: false });
26
27
 
27
- const MainLayer = Layer.mergeAll(BunServices.layer, ConfigServiceLayer, AuditServiceLayer);
28
+ const MainLayer = Layer.mergeAll(
29
+ BunServices.layer,
30
+ ConfigServiceLayer,
31
+ AuditServiceLayer,
32
+ SecretEnvResolver.liveLayer,
33
+ );
28
34
 
29
35
  const program = withRedactedOutput(withAudit("observability", cli)).pipe(
30
36
  Effect.provide(MainLayer),
@@ -5,7 +5,8 @@ import { ConfigService, getToolConfig } from "#config";
5
5
  import type { ObservabilityConfig } from "#config";
6
6
 
7
7
  import { ObservabilityToolError } from "./errors";
8
- import { registerResolvedSecret, unsafeOutboundTextReason } from "#shared/content-security";
8
+ import { redactSensitiveText, unsafeOutboundTextReason } from "#shared/content-security";
9
+ import { SecretEnvResolver } from "#shared/secret-env";
9
10
  import type {
10
11
  DsQueryOpts,
11
12
  DsQueryResponse,
@@ -48,18 +49,15 @@ export const profileOption = Flag.optional(
48
49
  ),
49
50
  );
50
51
 
51
- function resolveToken(tokenEnvVar?: string): string | undefined {
52
+ function resolveToken(
53
+ resolver: SecretEnvResolver["Service"],
54
+ tokenEnvVar?: string,
55
+ ): Promise<string | undefined> {
52
56
  if (!tokenEnvVar) {
53
- return undefined;
54
- }
55
-
56
- const token = process.env[tokenEnvVar];
57
- if (!token) {
58
- throw new Error(`${tokenEnvVar} environment variable is not set`);
57
+ return Promise.resolve(undefined);
59
58
  }
60
59
 
61
- registerResolvedSecret(token);
62
- return token;
60
+ return resolver.resolveSecretEnv(tokenEnvVar);
63
61
  }
64
62
 
65
63
  async function discoverDatasources(url: string, token?: string): Promise<GrafanaDatasource[]> {
@@ -68,7 +66,9 @@ async function discoverDatasources(url: string, token?: string): Promise<Grafana
68
66
 
69
67
  if (!response.ok) {
70
68
  const body = await response.text().catch(() => "");
71
- throw new Error(`Grafana API ${response.status}: /api/datasources — ${body}`);
69
+ throw new Error(
70
+ redactSensitiveText(`Grafana API ${response.status}: /api/datasources — ${body}`),
71
+ );
72
72
  }
73
73
 
74
74
  return (await response.json()) as GrafanaDatasource[];
@@ -102,13 +102,14 @@ export function requireTempoUid(
102
102
  async function resolveFromProfile(
103
103
  profile: ObservabilityConfig | undefined,
104
104
  env: string,
105
+ resolver: SecretEnvResolver["Service"],
105
106
  ): Promise<ObservabilityEnvConfig | undefined> {
106
107
  const environment = profile?.environments[env];
107
108
  if (!environment) {
108
109
  return undefined;
109
110
  }
110
111
 
111
- const token = resolveToken(environment.tokenEnvVar);
112
+ const token = await resolveToken(resolver, environment.tokenEnvVar);
112
113
  const datasources = await discoverDatasources(environment.url, token);
113
114
 
114
115
  // Tempo is optional: metrics/logs work on Grafana instances without it (e.g. Percona PMM).
@@ -157,6 +158,7 @@ function resolveFromEnv(
157
158
  export const resolveConfig = (env: string, profile: Option.Option<string>) =>
158
159
  Effect.gen(function* () {
159
160
  const config = yield* ConfigService;
161
+ const secretEnvResolver = yield* SecretEnvResolver;
160
162
  const profileName = Option.getOrUndefined(profile);
161
163
  const observabilityConfig = getToolConfig<ObservabilityConfig>(
162
164
  config,
@@ -166,7 +168,11 @@ export const resolveConfig = (env: string, profile: Option.Option<string>) =>
166
168
 
167
169
  return yield* Effect.tryPromise({
168
170
  try: async () => {
169
- const profileResolved = await resolveFromProfile(observabilityConfig, env);
171
+ const profileResolved = await resolveFromProfile(
172
+ observabilityConfig,
173
+ env,
174
+ secretEnvResolver,
175
+ );
170
176
  if (profileResolved) {
171
177
  return profileResolved;
172
178
  }
@@ -258,7 +264,7 @@ export function observabilityFetch<T>(
258
264
 
259
265
  if (!response.ok) {
260
266
  const body = await response.text().catch(() => "");
261
- throw new Error(`Grafana API ${response.status}: ${path} — ${body}`);
267
+ throw new Error(redactSensitiveText(`Grafana API ${response.status}: ${path} — ${body}`));
262
268
  }
263
269
 
264
270
  return (await response.json()) as T;
@@ -12,8 +12,13 @@ const MIN_SECRET_LENGTH = 8;
12
12
  const resolvedSecrets = new Set<string>();
13
13
 
14
14
  /** Redact a credential a tool resolved outside a sensitively named environment variable. */
15
- export const registerResolvedSecret = (value: string) => {
16
- if (value.length >= MIN_SECRET_LENGTH) resolvedSecrets.add(value);
15
+ export const registerResolvedSecret = (
16
+ value: string,
17
+ options: { readonly allowShort?: boolean } = {},
18
+ ) => {
19
+ if (value.length > 0 && (options.allowShort === true || value.length >= MIN_SECRET_LENGTH)) {
20
+ resolvedSecrets.add(value);
21
+ }
17
22
  };
18
23
 
19
24
  const sensitiveEnvironmentValues = (environment: Record<string, string | undefined>) =>
@@ -0,0 +1,230 @@
1
+ import { spawn } from "node:child_process";
2
+ import { tmpdir } from "node:os";
3
+ import { relative, resolve as resolvePath } from "node:path";
4
+ import { Context, Layer } from "effect";
5
+
6
+ import { registerResolvedSecret } from "./content-security";
7
+
8
+ const SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
9
+ const CONFIG_NAME_PATTERN = /^[A-Za-z0-9_]+$/;
10
+ const DEFAULT_TIMEOUT_MS = 10_000;
11
+ const STARTUP_ENV_NAMES = [
12
+ "PATH",
13
+ "HOME",
14
+ "USER",
15
+ "LOGNAME",
16
+ "SHELL",
17
+ "TERM",
18
+ "LANG",
19
+ "LC_ALL",
20
+ "TMPDIR",
21
+ "XDG_CONFIG_HOME",
22
+ "ZDOTDIR",
23
+ ] as const;
24
+
25
+ export type LookupResult =
26
+ | { readonly _tag: "found"; readonly value: string }
27
+ | { readonly _tag: "absent" }
28
+ | { readonly _tag: "failed"; readonly reason: string };
29
+
30
+ export type ZshRunner = (name: string, timeoutMs: number) => Promise<LookupResult>;
31
+
32
+ const failed = (name: string): LookupResult => ({ _tag: "failed", reason: name });
33
+
34
+ type FrameInspection =
35
+ | { readonly _tag: "incomplete" }
36
+ | { readonly _tag: "malformed" }
37
+ | { readonly _tag: "complete"; readonly result: LookupResult };
38
+
39
+ const inspectFrame = (frame: Buffer): FrameInspection => {
40
+ const firstNewline = frame.indexOf(10);
41
+ if (firstNewline < 0) {
42
+ const prefix = frame.toString("ascii");
43
+ return "set".startsWith(prefix) || "unset".startsWith(prefix)
44
+ ? { _tag: "incomplete" }
45
+ : { _tag: "malformed" };
46
+ }
47
+
48
+ const state = frame.subarray(0, firstNewline).toString("ascii");
49
+ if (state === "unset") {
50
+ return firstNewline === frame.length - 1
51
+ ? { _tag: "complete", result: { _tag: "absent" } }
52
+ : { _tag: "malformed" };
53
+ }
54
+ if (state !== "set") return { _tag: "malformed" };
55
+
56
+ const lengthEnd = frame.indexOf(10, firstNewline + 1);
57
+ if (lengthEnd < 0) {
58
+ const lengthPrefix = frame.subarray(firstNewline + 1).toString("ascii");
59
+ if (/^(0|[1-9][0-9]*)$/.test(lengthPrefix) || lengthPrefix === "") {
60
+ return { _tag: "incomplete" };
61
+ }
62
+ return { _tag: "malformed" };
63
+ }
64
+ const lengthText = frame.subarray(firstNewline + 1, lengthEnd).toString("ascii");
65
+ if (!/^(0|[1-9][0-9]*)$/.test(lengthText)) return { _tag: "malformed" };
66
+
67
+ const length = Number(lengthText);
68
+ const start = lengthEnd + 1;
69
+ if (!Number.isSafeInteger(length)) return { _tag: "malformed" };
70
+ if (start + length < frame.length) return { _tag: "malformed" };
71
+ if (start + length > frame.length) return { _tag: "incomplete" };
72
+ return {
73
+ _tag: "complete",
74
+ result: { _tag: "found", value: frame.subarray(start).toString("utf8") },
75
+ };
76
+ };
77
+
78
+ const runInteractiveZsh: ZshRunner = (name, timeoutMs) =>
79
+ new Promise((resolve) => {
80
+ if (process.env.VITEST !== undefined) {
81
+ const home = process.env.HOME;
82
+ const zdotdir = process.env.ZDOTDIR;
83
+ const relativeZdotdir = zdotdir ? relative(tmpdir(), resolvePath(zdotdir)) : "..";
84
+ if (
85
+ !home ||
86
+ !zdotdir ||
87
+ resolvePath(home) !== resolvePath(zdotdir) ||
88
+ relativeZdotdir === ".." ||
89
+ relativeZdotdir.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)
90
+ ) {
91
+ resolve(failed(name));
92
+ return;
93
+ }
94
+ }
95
+
96
+ const childEnv = Object.fromEntries(
97
+ STARTUP_ENV_NAMES.flatMap((key) => {
98
+ const value = process.env[key];
99
+ return value === undefined ? [] : [[key, value]];
100
+ }),
101
+ );
102
+ const script = `export LC_ALL=C; if (( \${+parameters[$1]} )); then value=\${(P)1}; print -ru3 -- "set"; print -ru3 -- "\${#value}"; print -rn -- "$value" >&3; else print -ru3 -- "unset"; fi`;
103
+
104
+ let child: ReturnType<typeof spawn>;
105
+ try {
106
+ child = spawn("zsh", ["-i", "-c", script, "agent-tools-secret-env", name], {
107
+ detached: true,
108
+ env: childEnv,
109
+ stdio: ["ignore", "ignore", "ignore", "pipe"],
110
+ });
111
+ } catch {
112
+ resolve(failed(name));
113
+ return;
114
+ }
115
+
116
+ const stream = child.stdio[3];
117
+ if (!stream || typeof stream === "number") {
118
+ child.kill("SIGKILL");
119
+ resolve(failed(name));
120
+ return;
121
+ }
122
+
123
+ const chunks: Buffer[] = [];
124
+ let exited = false;
125
+ let ended = false;
126
+ let exitCode: number | null = null;
127
+ let settled = false;
128
+ const killProcessGroup = () => {
129
+ if (child.pid === undefined) return;
130
+ try {
131
+ process.kill(-child.pid, "SIGKILL");
132
+ } catch {
133
+ // The process group may already have exited.
134
+ }
135
+ };
136
+ const finish = (result: LookupResult) => {
137
+ if (settled) return;
138
+ settled = true;
139
+ clearTimeout(timer);
140
+ stream.destroy();
141
+ killProcessGroup();
142
+ resolve(result);
143
+ };
144
+ const checkFrame = () => {
145
+ if (settled) return;
146
+ const inspection = inspectFrame(Buffer.concat(chunks));
147
+ if (inspection._tag === "malformed") return finish(failed(name));
148
+ if (!exited) return;
149
+ if (exitCode !== 0) return finish(failed(name));
150
+ if (inspection._tag === "complete") finish(inspection.result);
151
+ else if (ended) finish(failed(name));
152
+ };
153
+ const timer = setTimeout(() => {
154
+ finish(failed(name));
155
+ }, timeoutMs);
156
+
157
+ stream.on("data", (chunk: Buffer | string) => {
158
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
159
+ checkFrame();
160
+ });
161
+ stream.on("end", () => {
162
+ ended = true;
163
+ checkFrame();
164
+ });
165
+ stream.on("error", () => finish(failed(name)));
166
+ child.on("error", () => finish(failed(name)));
167
+ child.on("exit", (code) => {
168
+ exited = true;
169
+ exitCode = code;
170
+ checkFrame();
171
+ });
172
+ });
173
+
174
+ export class SecretEnvResolver extends Context.Service<
175
+ SecretEnvResolver,
176
+ {
177
+ readonly resolveSecretEnv: (name: string) => Promise<string>;
178
+ readonly lookupConfigEnv: (name: string) => Promise<LookupResult>;
179
+ }
180
+ >()("@agent-tools/SecretEnvResolver") {
181
+ static get liveLayer() {
182
+ return makeSecretEnvResolverLayer();
183
+ }
184
+ static readonly testLayer = (runner: ZshRunner = () => Promise.resolve({ _tag: "absent" })) =>
185
+ makeSecretEnvResolverLayer(runner);
186
+ }
187
+
188
+ export function makeSecretEnvResolverLayer(
189
+ runner: ZshRunner = runInteractiveZsh,
190
+ timeoutMs = DEFAULT_TIMEOUT_MS,
191
+ ) {
192
+ return Layer.sync(SecretEnvResolver, () => {
193
+ const cache = new Map<string, Promise<LookupResult>>();
194
+ const lookup = (name: string, emptyProcessValueWins: boolean): Promise<LookupResult> => {
195
+ const fromProcess = process.env[name];
196
+ if (fromProcess !== undefined && (emptyProcessValueWins || fromProcess !== "")) {
197
+ return Promise.resolve({ _tag: "found", value: fromProcess });
198
+ }
199
+
200
+ let result = cache.get(name);
201
+ if (!result) {
202
+ result = Promise.resolve()
203
+ .then(() => runner(name, timeoutMs))
204
+ .catch(() => failed(name));
205
+ cache.set(name, result);
206
+ }
207
+ return result;
208
+ };
209
+ const lookupConfigEnv = (name: string): Promise<LookupResult> => {
210
+ if (!CONFIG_NAME_PATTERN.test(name)) return Promise.resolve(failed(name));
211
+ return lookup(name, true);
212
+ };
213
+ const resolveSecretEnv = async (name: string): Promise<string> => {
214
+ if (!SECRET_NAME_PATTERN.test(name)) {
215
+ throw new Error(`Invalid environment variable name: ${name}`);
216
+ }
217
+ const result = await lookup(name, false);
218
+ if (result._tag !== "found") {
219
+ throw new Error(`${name} is not in the environment or in the interactive zsh`);
220
+ }
221
+ if (result.value.length < 4) {
222
+ throw new Error(`${name} is too short to redact safely`);
223
+ }
224
+ registerResolvedSecret(result.value, { allowShort: true });
225
+ return result.value;
226
+ };
227
+
228
+ return SecretEnvResolver.of({ resolveSecretEnv, lookupConfigEnv });
229
+ });
230
+ }