@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 +4 -1
- package/dist/db-tool/service.d.ts +2 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/db-tool/sql-client.d.ts +1 -0
- package/dist/db-tool/sql-client.d.ts.map +1 -1
- package/dist/observability-tool/logs.d.ts +1 -1
- package/dist/observability-tool/logs.d.ts.map +1 -1
- package/dist/observability-tool/metrics.d.ts +1 -1
- package/dist/observability-tool/metrics.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts +2 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/observability-tool/trace.d.ts +1 -1
- package/dist/observability-tool/trace.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +3 -1
- package/dist/shared/content-security.d.ts.map +1 -1
- package/dist/shared/secret-env.d.ts +22 -0
- package/dist/shared/secret-env.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/db-tool/main.ts +2 -0
- package/src/db-tool/service.ts +122 -140
- package/src/db-tool/sql-client.ts +7 -4
- package/src/observability-tool/main.ts +7 -1
- package/src/observability-tool/shared.ts +20 -14
- package/src/shared/content-security.ts +7 -2
- package/src/shared/secret-env.ts +230 -0
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
|
|
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,
|
|
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;
|
|
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,
|
|
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,
|
|
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;
|
|
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,
|
|
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
|
|
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,
|
|
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
package/src/db-tool/main.ts
CHANGED
|
@@ -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),
|
package/src/db-tool/service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
2
|
-
import { Clock, Context, Duration, Effect, Layer,
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
288
|
+
if (!portOverride.ok) {
|
|
289
|
+
return yield* new DbConnectionError({
|
|
290
|
+
message: portOverride.message,
|
|
291
|
+
environment: env,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
312
294
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 = (
|
|
16
|
-
|
|
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
|
+
}
|