@blogic-cz/agent-tools 0.14.44 → 0.14.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.14.44",
3
+ "version": "0.14.45",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -291,6 +291,13 @@
291
291
  "$ref": "#/definitions/DbMutationOperation"
292
292
  }
293
293
  }
294
+ },
295
+ "allowedMutationTargets": {
296
+ "description": "Explicitly allowed SQL mutation targets per environment and operation. Use normalized schema-qualified names such as ticker.TimeTickers.",
297
+ "type": "object",
298
+ "additionalProperties": {
299
+ "$ref": "#/definitions/DbAllowedMutationTargets"
300
+ }
294
301
  }
295
302
  },
296
303
  "required": ["environments"]
@@ -591,6 +598,30 @@
591
598
  "description": "SQL mutation operation that can be explicitly allowed for a database environment.",
592
599
  "type": "string",
593
600
  "enum": ["insert", "update", "delete"]
601
+ },
602
+ "DbAllowedMutationTargets": {
603
+ "type": "object",
604
+ "additionalProperties": false,
605
+ "properties": {
606
+ "insert": {
607
+ "type": "array",
608
+ "items": {
609
+ "type": "string"
610
+ }
611
+ },
612
+ "update": {
613
+ "type": "array",
614
+ "items": {
615
+ "type": "string"
616
+ }
617
+ },
618
+ "delete": {
619
+ "type": "array",
620
+ "items": {
621
+ "type": "string"
622
+ }
623
+ }
624
+ }
594
625
  }
595
626
  },
596
627
  "examples": [
@@ -640,6 +671,11 @@
640
671
  "remotePort": 5432,
641
672
  "allowedMutations": {
642
673
  "test": ["insert"]
674
+ },
675
+ "allowedMutationTargets": {
676
+ "prod": {
677
+ "insert": ["ticker.TimeTickers"]
678
+ }
643
679
  }
644
680
  }
645
681
  },
@@ -3,6 +3,7 @@ export type {
3
3
  AzureConfig,
4
4
  K8sConfig,
5
5
  DbEnvConfig,
6
+ DbAllowedMutationTargets,
6
7
  DbMutationOperation,
7
8
  DatabaseConfig,
8
9
  ObservabilityConfig,
@@ -96,11 +96,20 @@ const DbEnvConfigSchema = Schema.Struct({
96
96
  vpn: Schema.optionalKey(Schema.String),
97
97
  });
98
98
 
99
+ const DbAllowedMutationTargetsSchema = Schema.Struct({
100
+ insert: Schema.optionalKey(Schema.Array(Schema.String)),
101
+ update: Schema.optionalKey(Schema.Array(Schema.String)),
102
+ delete: Schema.optionalKey(Schema.Array(Schema.String)),
103
+ });
104
+
99
105
  const DatabaseConfigSchema = Schema.Struct({
100
106
  environments: Schema.Record(Schema.String, DbEnvConfigSchema),
101
107
  allowedMutations: Schema.optionalKey(
102
108
  Schema.Record(Schema.String, Schema.Array(DbMutationOperationSchema)),
103
109
  ),
110
+ allowedMutationTargets: Schema.optionalKey(
111
+ Schema.Record(Schema.String, DbAllowedMutationTargetsSchema),
112
+ ),
104
113
  kubectl: Schema.optionalKey(
105
114
  Schema.Struct({
106
115
  kubeconfig: Schema.optionalKey(Schema.String),
@@ -95,6 +95,7 @@ export type DbEnvConfig = ProfilePrerequisites & {
95
95
  /** SQL mutation operation that can be explicitly allowed for a database environment. */
96
96
  export const DbMutationOperationSchema = Schema.Literals(["insert", "update", "delete"]);
97
97
  export type DbMutationOperation = Schema.Schema.Type<typeof DbMutationOperationSchema>;
98
+ export type DbAllowedMutationTargets = Partial<Record<DbMutationOperation, readonly string[]>>;
98
99
 
99
100
  /** Database profile configuration */
100
101
  export type DatabaseConfig = ProfilePrerequisites & {
@@ -102,6 +103,7 @@ export type DatabaseConfig = ProfilePrerequisites & {
102
103
  environments: Record<string, DbEnvConfig>;
103
104
  /** Explicitly allowed SQL mutation operations per environment. Non-local environments default to read-only. */
104
105
  allowedMutations?: Record<string, readonly DbMutationOperation[]>;
106
+ allowedMutationTargets?: Record<string, DbAllowedMutationTargets>;
105
107
  kubectl?: {
106
108
  /** Optional kubeconfig path. Supports ${ENV_VAR} templates. */
107
109
  kubeconfig?: string;
@@ -17,6 +17,13 @@ const ALLOWABLE_MUTATION_PATTERNS: Array<[DbMutationOperation, RegExp]> = [
17
17
  ];
18
18
 
19
19
  const TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
20
+ const IDENTIFIER_PATTERN = String.raw`(?:"[^"]+"|[a-zA-Z_][a-zA-Z0-9_]*)`;
21
+ const QUALIFIED_IDENTIFIER_PATTERN = `${IDENTIFIER_PATTERN}(?:\\s*\\.\\s*${IDENTIFIER_PATTERN})?`;
22
+ const MUTATION_TARGET_PATTERNS: Array<[DbMutationOperation, RegExp]> = [
23
+ ["insert", new RegExp(String.raw`^\s*INSERT\s+INTO\s+(${QUALIFIED_IDENTIFIER_PATTERN})`, "i")],
24
+ ["update", new RegExp(String.raw`^\s*UPDATE\s+(${QUALIFIED_IDENTIFIER_PATTERN})`, "i")],
25
+ ["delete", new RegExp(String.raw`^\s*DELETE\s+FROM\s+(${QUALIFIED_IDENTIFIER_PATTERN})`, "i")],
26
+ ];
20
27
 
21
28
  /**
22
29
  * Strip SQL comments (block and line) while preserving string literals.
@@ -89,6 +96,27 @@ export function getAllowedMutationOperation(sql: string): DbMutationOperation |
89
96
  return ALLOWABLE_MUTATION_PATTERNS.find(([, pattern]) => pattern.test(stripped))?.[0];
90
97
  }
91
98
 
99
+ function normalizeSqlIdentifier(identifier: string): string {
100
+ return identifier
101
+ .split(".")
102
+ .map((part) =>
103
+ part
104
+ .trim()
105
+ .replace(/^"(.+)"$/, "$1")
106
+ .replace(/""/g, '"'),
107
+ )
108
+ .join(".");
109
+ }
110
+
111
+ export function getMutationTarget(sql: string): string | undefined {
112
+ const stripped = stripSqlComments(sql);
113
+ const operation = getAllowedMutationOperation(stripped);
114
+ const pattern = MUTATION_TARGET_PATTERNS.find(([candidate]) => candidate === operation)?.[1];
115
+ const target = pattern?.exec(stripped)?.[1];
116
+
117
+ return target === undefined ? undefined : normalizeSqlIdentifier(target);
118
+ }
119
+
92
120
  export function isValidTableName(tableName: string): boolean {
93
121
  return TABLE_NAME_PATTERN.test(tableName);
94
122
  }
@@ -28,6 +28,7 @@ import {
28
28
  import {
29
29
  detectSchemaError,
30
30
  getAllowedMutationOperation,
31
+ getMutationTarget,
31
32
  isValidTableName,
32
33
  isMutationQuery,
33
34
  } from "./security";
@@ -712,6 +713,7 @@ export class DbService extends Context.Service<
712
713
  needsTunnel: accessMode.needsTunnel,
713
714
  allowMutations: accessMode.allowMutations,
714
715
  allowedMutations: accessMode.allowedMutations,
716
+ allowedMutationTargets: dbConfig.allowedMutationTargets?.[env] ?? {},
715
717
  };
716
718
  });
717
719
 
@@ -725,11 +727,17 @@ export class DbService extends Context.Service<
725
727
  const password = yield* resolvePassword(resolvedConfig, env);
726
728
  const mutation = isMutationQuery(sql);
727
729
  const mutationOperation = mutation ? getAllowedMutationOperation(sql) : undefined;
730
+ const mutationTarget = mutation ? getMutationTarget(sql) : undefined;
728
731
  const mutationAllowed =
729
732
  !mutation ||
730
733
  resolvedConfig.allowMutations ||
731
734
  (mutationOperation !== undefined &&
732
- resolvedConfig.allowedMutations.includes(mutationOperation));
735
+ resolvedConfig.allowedMutations.includes(mutationOperation)) ||
736
+ (mutationOperation !== undefined &&
737
+ mutationTarget !== undefined &&
738
+ (resolvedConfig.allowedMutationTargets[mutationOperation] ?? []).includes(
739
+ mutationTarget,
740
+ ));
733
741
 
734
742
  if (!mutationAllowed) {
735
743
  const allowed =
@@ -739,7 +747,7 @@ export class DbService extends Context.Service<
739
747
  return yield* new DbMutationBlockedError({
740
748
  message: `Mutation queries are not allowed on environment ${env}. Allowed mutation operations: ${allowed}.`,
741
749
  environment: env,
742
- hint: 'Configure database.<profile>.allowedMutations.<env> with explicit operations such as ["insert"] if this environment should allow controlled mutations.',
750
+ hint: 'Configure database.<profile>.allowedMutationTargets.<env> with explicit targets such as { insert: ["ticker.TimeTickers"] }, or allowedMutations.<env> for broader operation-level access.',
743
751
  });
744
752
  }
745
753
 
@@ -1,5 +1,8 @@
1
- import type { DbMutationOperation } from "#config";
2
- import type { ProfilePrerequisites } from "#config/types";
1
+ import type {
2
+ DbAllowedMutationTargets,
3
+ DbMutationOperation,
4
+ ProfilePrerequisites,
5
+ } from "#config/types";
3
6
  import type { Environment, OutputFormat } from "#shared";
4
7
 
5
8
  export type { DbMutationOperation };
@@ -17,6 +20,7 @@ export type DbConfig = ProfilePrerequisites & {
17
20
  needsTunnel: boolean;
18
21
  allowMutations: boolean;
19
22
  allowedMutations: readonly DbMutationOperation[];
23
+ allowedMutationTargets: DbAllowedMutationTargets;
20
24
  };
21
25
 
22
26
  export type QueryResult = {