@carthooks/arcubase-cli 0.1.12 → 0.1.13

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.
@@ -6660,8 +6660,9 @@ function renderCommandHelp(scope, moduleName, commandName, env = process.env) {
6660
6660
  ' - update policy: {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}'
6661
6661
  ] : [],
6662
6662
  ...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "create" ? [
6663
- "body-json example:",
6664
- ' - {"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":":1001","read":true,"write":true,"report":false},{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
6663
+ "body-json examples:",
6664
+ ` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
6665
+ ` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
6665
6666
  "success requires:",
6666
6667
  " - result.enabled must be true",
6667
6668
  ' - options.user_scope[].type is "user" for tenant users',
@@ -7040,6 +7041,26 @@ function isAccessRuleUpdateCommand(scope, command) {
7040
7041
  return scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update";
7041
7042
  }
7042
7043
  function buildAccessRuleShapeHint(userId = 2188889901) {
7044
+ return buildHiddenFieldAccessRuleBody(userId);
7045
+ }
7046
+ function buildFullAccessRuleBody(userId = 2188889977) {
7047
+ return {
7048
+ name: "Admin full access",
7049
+ enabled: true,
7050
+ type: "form",
7051
+ options: {
7052
+ user_scope: [{ type: "user", id: userId }],
7053
+ policy: {
7054
+ key: "custom",
7055
+ actions: ["view", "add", "edit", "delete"],
7056
+ all_fields_read: true,
7057
+ all_fields_write: true
7058
+ },
7059
+ list_options: {}
7060
+ }
7061
+ };
7062
+ }
7063
+ function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
7043
7064
  return {
7044
7065
  name: "Sales read write",
7045
7066
  enabled: true,
@@ -7057,6 +7078,12 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
7057
7078
  }
7058
7079
  };
7059
7080
  }
7081
+ function buildAccessRuleCreateToolCall(body) {
7082
+ return {
7083
+ command: "access-rule create",
7084
+ args: ["--app-id", "<app_id>", "--table-id", "<table_id>", "--body-json", JSON.stringify(body)]
7085
+ };
7086
+ }
7060
7087
  function buildAssignUsersValidationDetails(scope, command, requestType, path, message, env = process.env) {
7061
7088
  return {
7062
7089
  operation: `${scope}.${command.module}.${command.functionName}`,
@@ -7194,7 +7221,15 @@ function buildBodyGuidance(requestType) {
7194
7221
  };
7195
7222
  }
7196
7223
  if (requestType === "AppIngressCreateReqVO" || requestType === "AppIngressUpdateReqVO") {
7224
+ const retryToolCalls = [
7225
+ buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
7226
+ buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody())
7227
+ ];
7197
7228
  return {
7229
+ retryToolCalls,
7230
+ suggestions: [
7231
+ "next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access"
7232
+ ],
7198
7233
  shapeHint: buildAccessRuleShapeHint(),
7199
7234
  commonMistakes: [
7200
7235
  "use options.policy, not top-level permissions",
@@ -7647,6 +7682,60 @@ function unwrapData(value) {
7647
7682
  }
7648
7683
  return current;
7649
7684
  }
7685
+ function numericFieldKeys(fields) {
7686
+ if (!isRecord2(fields)) return [];
7687
+ return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right));
7688
+ }
7689
+ function permitFieldId(permit) {
7690
+ if (!isRecord2(permit) || typeof permit.key !== "string") return void 0;
7691
+ const match = /^:(\d+)$/.exec(permit.key);
7692
+ return match?.[1];
7693
+ }
7694
+ function rowFieldPermits(value) {
7695
+ const data = unwrapData(value);
7696
+ const fieldPermits = isRecord2(data) && isRecord2(data.fieldPermits) ? data.fieldPermits : void 0;
7697
+ return fieldPermits && Array.isArray(fieldPermits.fields) ? fieldPermits.fields.filter((item) => isRecord2(item)) : [];
7698
+ }
7699
+ function buildRowValueEvidence(command, responseBody) {
7700
+ if (command.scope !== "user" || command.commandPath[0] !== "row") return void 0;
7701
+ const action = command.commandPath[1];
7702
+ const data = unwrapData(responseBody);
7703
+ const rules = [
7704
+ "current row values come only from row query fields or row get record.fields",
7705
+ "table schema fields[].value and default_value_mode are schema defaults, not returned row values",
7706
+ "a field id absent from returned row fields has no row-value evidence",
7707
+ "read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error"
7708
+ ];
7709
+ if (action === "get") {
7710
+ const record = isRecord2(data) && isRecord2(data.record) ? data.record : void 0;
7711
+ const returnedFieldIds = numericFieldKeys(record?.fields);
7712
+ const permits = rowFieldPermits(responseBody);
7713
+ const permitFieldIds = permits.map(permitFieldId).filter((id) => Boolean(id));
7714
+ const deniedReadFieldIds = permits.filter((permit) => permit.read === false).map(permitFieldId).filter((id) => Boolean(id)).sort((left, right) => Number(left) - Number(right));
7715
+ const notReturnedFieldIds = permitFieldIds.filter((id) => !returnedFieldIds.includes(id)).sort((left, right) => Number(left) - Number(right));
7716
+ return {
7717
+ source: "row get record.fields",
7718
+ returnedFieldIds,
7719
+ notReturnedFieldIds,
7720
+ deniedReadFieldIds,
7721
+ absentFieldMeaning: "not returned to current user; no row-value evidence",
7722
+ rules
7723
+ };
7724
+ }
7725
+ if (action === "query") {
7726
+ const items = isRecord2(data) && Array.isArray(data.items) ? data.items : [];
7727
+ return {
7728
+ source: "row query items[].fields",
7729
+ rows: items.filter((item) => isRecord2(item)).map((item) => ({
7730
+ rowId: item.id,
7731
+ returnedFieldIds: numericFieldKeys(item.fields)
7732
+ })),
7733
+ absentFieldMeaning: "not returned to current user; no row-value evidence",
7734
+ rules
7735
+ };
7736
+ }
7737
+ return void 0;
7738
+ }
7650
7739
  function requiredRowAction(command, flags) {
7651
7740
  if (command.commandPath[0] !== "row") return void 0;
7652
7741
  switch (command.commandPath[1]) {
@@ -7954,7 +8043,7 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
7954
8043
  if (error instanceof CLIError && error.code === "INVALID_BODY_JSON" && command.requestType) {
7955
8044
  throw new CLIError(
7956
8045
  "INVALID_BODY_JSON",
7957
- `body json is invalid for ${scope}.${command.commandPath.join(" ")}; fix JSON syntax, then use the canonical ${command.requestType} shape from docs/types`,
8046
+ `body json is invalid for ${scope}.${command.commandPath.join(" ")}; retry with retryToolCalls[0] or retryToolCalls[1] when present`,
7958
8047
  2,
7959
8048
  buildBodyValidationDetails(scope, command, command.requestType, [{ path: "body", message: "invalid JSON syntax" }], runtimeEnv)
7960
8049
  );
@@ -8091,11 +8180,13 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
8091
8180
  } : {}
8092
8181
  });
8093
8182
  }
8183
+ const rowValueEvidence = buildRowValueEvidence(command, parsedResponse);
8094
8184
  return {
8095
8185
  kind: "result",
8096
8186
  commandPath: command.commandPath,
8097
8187
  status: response.status,
8098
- data: parsedResponse
8188
+ data: parsedResponse,
8189
+ ...rowValueEvidence ? { rowValueEvidence } : {}
8099
8190
  };
8100
8191
  }
8101
8192
 
@@ -6660,8 +6660,9 @@ function renderCommandHelp(scope, moduleName, commandName, env = process.env) {
6660
6660
  ' - update policy: {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}'
6661
6661
  ] : [],
6662
6662
  ...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "create" ? [
6663
- "body-json example:",
6664
- ' - {"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":":1001","read":true,"write":true,"report":false},{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
6663
+ "body-json examples:",
6664
+ ` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
6665
+ ` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
6665
6666
  "success requires:",
6666
6667
  " - result.enabled must be true",
6667
6668
  ' - options.user_scope[].type is "user" for tenant users',
@@ -7040,6 +7041,26 @@ function isAccessRuleUpdateCommand(scope, command) {
7040
7041
  return scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update";
7041
7042
  }
7042
7043
  function buildAccessRuleShapeHint(userId = 2188889901) {
7044
+ return buildHiddenFieldAccessRuleBody(userId);
7045
+ }
7046
+ function buildFullAccessRuleBody(userId = 2188889977) {
7047
+ return {
7048
+ name: "Admin full access",
7049
+ enabled: true,
7050
+ type: "form",
7051
+ options: {
7052
+ user_scope: [{ type: "user", id: userId }],
7053
+ policy: {
7054
+ key: "custom",
7055
+ actions: ["view", "add", "edit", "delete"],
7056
+ all_fields_read: true,
7057
+ all_fields_write: true
7058
+ },
7059
+ list_options: {}
7060
+ }
7061
+ };
7062
+ }
7063
+ function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
7043
7064
  return {
7044
7065
  name: "Sales read write",
7045
7066
  enabled: true,
@@ -7057,6 +7078,12 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
7057
7078
  }
7058
7079
  };
7059
7080
  }
7081
+ function buildAccessRuleCreateToolCall(body) {
7082
+ return {
7083
+ command: "access-rule create",
7084
+ args: ["--app-id", "<app_id>", "--table-id", "<table_id>", "--body-json", JSON.stringify(body)]
7085
+ };
7086
+ }
7060
7087
  function buildAssignUsersValidationDetails(scope, command, requestType, path, message, env = process.env) {
7061
7088
  return {
7062
7089
  operation: `${scope}.${command.module}.${command.functionName}`,
@@ -7194,7 +7221,15 @@ function buildBodyGuidance(requestType) {
7194
7221
  };
7195
7222
  }
7196
7223
  if (requestType === "AppIngressCreateReqVO" || requestType === "AppIngressUpdateReqVO") {
7224
+ const retryToolCalls = [
7225
+ buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
7226
+ buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody())
7227
+ ];
7197
7228
  return {
7229
+ retryToolCalls,
7230
+ suggestions: [
7231
+ "next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access"
7232
+ ],
7198
7233
  shapeHint: buildAccessRuleShapeHint(),
7199
7234
  commonMistakes: [
7200
7235
  "use options.policy, not top-level permissions",
@@ -7647,6 +7682,60 @@ function unwrapData(value) {
7647
7682
  }
7648
7683
  return current;
7649
7684
  }
7685
+ function numericFieldKeys(fields) {
7686
+ if (!isRecord2(fields)) return [];
7687
+ return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right));
7688
+ }
7689
+ function permitFieldId(permit) {
7690
+ if (!isRecord2(permit) || typeof permit.key !== "string") return void 0;
7691
+ const match = /^:(\d+)$/.exec(permit.key);
7692
+ return match?.[1];
7693
+ }
7694
+ function rowFieldPermits(value) {
7695
+ const data = unwrapData(value);
7696
+ const fieldPermits = isRecord2(data) && isRecord2(data.fieldPermits) ? data.fieldPermits : void 0;
7697
+ return fieldPermits && Array.isArray(fieldPermits.fields) ? fieldPermits.fields.filter((item) => isRecord2(item)) : [];
7698
+ }
7699
+ function buildRowValueEvidence(command, responseBody) {
7700
+ if (command.scope !== "user" || command.commandPath[0] !== "row") return void 0;
7701
+ const action = command.commandPath[1];
7702
+ const data = unwrapData(responseBody);
7703
+ const rules = [
7704
+ "current row values come only from row query fields or row get record.fields",
7705
+ "table schema fields[].value and default_value_mode are schema defaults, not returned row values",
7706
+ "a field id absent from returned row fields has no row-value evidence",
7707
+ "read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error"
7708
+ ];
7709
+ if (action === "get") {
7710
+ const record = isRecord2(data) && isRecord2(data.record) ? data.record : void 0;
7711
+ const returnedFieldIds = numericFieldKeys(record?.fields);
7712
+ const permits = rowFieldPermits(responseBody);
7713
+ const permitFieldIds = permits.map(permitFieldId).filter((id) => Boolean(id));
7714
+ const deniedReadFieldIds = permits.filter((permit) => permit.read === false).map(permitFieldId).filter((id) => Boolean(id)).sort((left, right) => Number(left) - Number(right));
7715
+ const notReturnedFieldIds = permitFieldIds.filter((id) => !returnedFieldIds.includes(id)).sort((left, right) => Number(left) - Number(right));
7716
+ return {
7717
+ source: "row get record.fields",
7718
+ returnedFieldIds,
7719
+ notReturnedFieldIds,
7720
+ deniedReadFieldIds,
7721
+ absentFieldMeaning: "not returned to current user; no row-value evidence",
7722
+ rules
7723
+ };
7724
+ }
7725
+ if (action === "query") {
7726
+ const items = isRecord2(data) && Array.isArray(data.items) ? data.items : [];
7727
+ return {
7728
+ source: "row query items[].fields",
7729
+ rows: items.filter((item) => isRecord2(item)).map((item) => ({
7730
+ rowId: item.id,
7731
+ returnedFieldIds: numericFieldKeys(item.fields)
7732
+ })),
7733
+ absentFieldMeaning: "not returned to current user; no row-value evidence",
7734
+ rules
7735
+ };
7736
+ }
7737
+ return void 0;
7738
+ }
7650
7739
  function requiredRowAction(command, flags) {
7651
7740
  if (command.commandPath[0] !== "row") return void 0;
7652
7741
  switch (command.commandPath[1]) {
@@ -7954,7 +8043,7 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
7954
8043
  if (error instanceof CLIError && error.code === "INVALID_BODY_JSON" && command.requestType) {
7955
8044
  throw new CLIError(
7956
8045
  "INVALID_BODY_JSON",
7957
- `body json is invalid for ${scope}.${command.commandPath.join(" ")}; fix JSON syntax, then use the canonical ${command.requestType} shape from docs/types`,
8046
+ `body json is invalid for ${scope}.${command.commandPath.join(" ")}; retry with retryToolCalls[0] or retryToolCalls[1] when present`,
7958
8047
  2,
7959
8048
  buildBodyValidationDetails(scope, command, command.requestType, [{ path: "body", message: "invalid JSON syntax" }], runtimeEnv)
7960
8049
  );
@@ -8091,11 +8180,13 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
8091
8180
  } : {}
8092
8181
  });
8093
8182
  }
8183
+ const rowValueEvidence = buildRowValueEvidence(command, parsedResponse);
8094
8184
  return {
8095
8185
  kind: "result",
8096
8186
  commandPath: command.commandPath,
8097
8187
  status: response.status,
8098
- data: parsedResponse
8188
+ data: parsedResponse,
8189
+ ...rowValueEvidence ? { rowValueEvidence } : {}
8099
8190
  };
8100
8191
  }
8101
8192
 
@@ -26,6 +26,7 @@ export type CLIErrorDetails = {
26
26
  responseBody?: string;
27
27
  traceId?: string;
28
28
  upstreamError?: unknown;
29
+ retryToolCalls?: unknown[];
29
30
  suggestions?: string[];
30
31
  };
31
32
  export declare class CLIError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/runtime/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC7B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;IACzB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB,CAAA;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAA;gBAEtB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI,EAAE,OAAO,CAAC,EAAE,eAAe;IAQlF,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASlC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/runtime/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC7B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;IACzB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,cAAc,CAAC,EAAE,OAAO,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB,CAAA;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAA;gBAEtB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI,EAAE,OAAO,CAAC,EAAE,eAAe;IAQlF,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASlC"}
@@ -1 +1 @@
1
- {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/runtime/execute.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1D,OAAO,EAAwE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAK/H,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B,CAAA;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAc1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAoB7G;AAOD,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAMlD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAkFpI;AAu5CD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,uBAAuB,CActH;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAiMrI"}
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/runtime/execute.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1D,OAAO,EAAwE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAK/H,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B,CAAA;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAc1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAoB7G;AAOD,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAMlD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAmFpI;AA8/CD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,uBAAuB,CActH;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAmMrI"}
@@ -104,8 +104,9 @@ export function renderCommandHelp(scope, moduleName, commandName, env = process.
104
104
  : []),
105
105
  ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'create'
106
106
  ? [
107
- 'body-json example:',
108
- ' - {"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":":1001","read":true,"write":true,"report":false},{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
107
+ 'body-json examples:',
108
+ ` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
109
+ ` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
109
110
  'success requires:',
110
111
  ' - result.enabled must be true',
111
112
  ' - options.user_scope[].type is "user" for tenant users',
@@ -510,6 +511,26 @@ function isAccessRuleUpdateCommand(scope, command) {
510
511
  return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update';
511
512
  }
512
513
  function buildAccessRuleShapeHint(userId = 2188889901) {
514
+ return buildHiddenFieldAccessRuleBody(userId);
515
+ }
516
+ function buildFullAccessRuleBody(userId = 2188889977) {
517
+ return {
518
+ name: 'Admin full access',
519
+ enabled: true,
520
+ type: 'form',
521
+ options: {
522
+ user_scope: [{ type: 'user', id: userId }],
523
+ policy: {
524
+ key: 'custom',
525
+ actions: ['view', 'add', 'edit', 'delete'],
526
+ all_fields_read: true,
527
+ all_fields_write: true,
528
+ },
529
+ list_options: {},
530
+ },
531
+ };
532
+ }
533
+ function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
513
534
  return {
514
535
  name: 'Sales read write',
515
536
  enabled: true,
@@ -527,6 +548,12 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
527
548
  },
528
549
  };
529
550
  }
551
+ function buildAccessRuleCreateToolCall(body) {
552
+ return {
553
+ command: 'access-rule create',
554
+ args: ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json', JSON.stringify(body)],
555
+ };
556
+ }
530
557
  function buildAssignUsersValidationDetails(scope, command, requestType, path, message, env = process.env) {
531
558
  return {
532
559
  operation: `${scope}.${command.module}.${command.functionName}`,
@@ -664,7 +691,15 @@ function buildBodyGuidance(requestType) {
664
691
  };
665
692
  }
666
693
  if (requestType === 'AppIngressCreateReqVO' || requestType === 'AppIngressUpdateReqVO') {
694
+ const retryToolCalls = [
695
+ buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
696
+ buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody()),
697
+ ];
667
698
  return {
699
+ retryToolCalls,
700
+ suggestions: [
701
+ 'next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access',
702
+ ],
668
703
  shapeHint: buildAccessRuleShapeHint(),
669
704
  commonMistakes: [
670
705
  'use options.policy, not top-level permissions',
@@ -938,6 +973,71 @@ function unwrapData(value) {
938
973
  }
939
974
  return current;
940
975
  }
976
+ function numericFieldKeys(fields) {
977
+ if (!isRecord(fields))
978
+ return [];
979
+ return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right));
980
+ }
981
+ function permitFieldId(permit) {
982
+ if (!isRecord(permit) || typeof permit.key !== 'string')
983
+ return undefined;
984
+ const match = /^:(\d+)$/.exec(permit.key);
985
+ return match?.[1];
986
+ }
987
+ function rowFieldPermits(value) {
988
+ const data = unwrapData(value);
989
+ const fieldPermits = isRecord(data) && isRecord(data.fieldPermits) ? data.fieldPermits : undefined;
990
+ return fieldPermits && Array.isArray(fieldPermits.fields)
991
+ ? fieldPermits.fields.filter((item) => isRecord(item))
992
+ : [];
993
+ }
994
+ function buildRowValueEvidence(command, responseBody) {
995
+ if (command.scope !== 'user' || command.commandPath[0] !== 'row')
996
+ return undefined;
997
+ const action = command.commandPath[1];
998
+ const data = unwrapData(responseBody);
999
+ const rules = [
1000
+ 'current row values come only from row query fields or row get record.fields',
1001
+ 'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
1002
+ 'a field id absent from returned row fields has no row-value evidence',
1003
+ 'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
1004
+ ];
1005
+ if (action === 'get') {
1006
+ const record = isRecord(data) && isRecord(data.record) ? data.record : undefined;
1007
+ const returnedFieldIds = numericFieldKeys(record?.fields);
1008
+ const permits = rowFieldPermits(responseBody);
1009
+ const permitFieldIds = permits.map(permitFieldId).filter((id) => Boolean(id));
1010
+ const deniedReadFieldIds = permits
1011
+ .filter((permit) => permit.read === false)
1012
+ .map(permitFieldId)
1013
+ .filter((id) => Boolean(id))
1014
+ .sort((left, right) => Number(left) - Number(right));
1015
+ const notReturnedFieldIds = permitFieldIds
1016
+ .filter((id) => !returnedFieldIds.includes(id))
1017
+ .sort((left, right) => Number(left) - Number(right));
1018
+ return {
1019
+ source: 'row get record.fields',
1020
+ returnedFieldIds,
1021
+ notReturnedFieldIds,
1022
+ deniedReadFieldIds,
1023
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
1024
+ rules,
1025
+ };
1026
+ }
1027
+ if (action === 'query') {
1028
+ const items = isRecord(data) && Array.isArray(data.items) ? data.items : [];
1029
+ return {
1030
+ source: 'row query items[].fields',
1031
+ rows: items.filter((item) => isRecord(item)).map((item) => ({
1032
+ rowId: item.id,
1033
+ returnedFieldIds: numericFieldKeys(item.fields),
1034
+ })),
1035
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
1036
+ rules,
1037
+ };
1038
+ }
1039
+ return undefined;
1040
+ }
941
1041
  function requiredRowAction(command, flags) {
942
1042
  if (command.commandPath[0] !== 'row')
943
1043
  return undefined;
@@ -1272,7 +1372,7 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
1272
1372
  }
1273
1373
  catch (error) {
1274
1374
  if (error instanceof CLIError && error.code === 'INVALID_BODY_JSON' && command.requestType) {
1275
- throw new CLIError('INVALID_BODY_JSON', `body json is invalid for ${scope}.${command.commandPath.join(' ')}; fix JSON syntax, then use the canonical ${command.requestType} shape from docs/types`, 2, buildBodyValidationDetails(scope, command, command.requestType, [{ path: 'body', message: 'invalid JSON syntax' }], runtimeEnv));
1375
+ throw new CLIError('INVALID_BODY_JSON', `body json is invalid for ${scope}.${command.commandPath.join(' ')}; retry with retryToolCalls[0] or retryToolCalls[1] when present`, 2, buildBodyValidationDetails(scope, command, command.requestType, [{ path: 'body', message: 'invalid JSON syntax' }], runtimeEnv));
1276
1376
  }
1277
1377
  throw error;
1278
1378
  }
@@ -1381,10 +1481,12 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
1381
1481
  : {}),
1382
1482
  });
1383
1483
  }
1484
+ const rowValueEvidence = buildRowValueEvidence(command, parsedResponse);
1384
1485
  return {
1385
1486
  kind: 'result',
1386
1487
  commandPath: command.commandPath,
1387
1488
  status: response.status,
1388
1489
  data: parsedResponse,
1490
+ ...(rowValueEvidence ? { rowValueEvidence } : {}),
1389
1491
  };
1390
1492
  }
@@ -250,6 +250,37 @@ test('body json parse errors include command docs and type hints', async () => {
250
250
  return true;
251
251
  });
252
252
  });
253
+ test('access-rule invalid json returns copyable create retry examples', async () => {
254
+ await assert.rejects(async () => {
255
+ await executeCLI('admin', ['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"Admin full access","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889977}],"policy":{"key":"custom","actions":["view","add","edit","delete"],"all_fields_read":true,"all_fields_write":true},"list_options":{}}'], env, async () => new Response('{}', { status: 200 }));
256
+ }, (error) => {
257
+ assert.ok(error instanceof CLIError);
258
+ assert.equal(error.code, 'INVALID_BODY_JSON');
259
+ assert.equal(error.details?.requestType, 'AppIngressCreateReqVO');
260
+ assert.match(error.message, /retryToolCalls/);
261
+ const toolCall = (error.details?.retryToolCalls ?? [])[0];
262
+ assert.ok(toolCall);
263
+ assert.equal(toolCall.command, 'access-rule create');
264
+ assert.deepEqual(toolCall.args.slice(0, 5), ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json']);
265
+ const body = JSON.parse(toolCall.args[5]);
266
+ assert.deepEqual(body, {
267
+ name: 'Admin full access',
268
+ enabled: true,
269
+ type: 'form',
270
+ options: {
271
+ user_scope: [{ type: 'user', id: 2188889977 }],
272
+ policy: {
273
+ key: 'custom',
274
+ actions: ['view', 'add', 'edit', 'delete'],
275
+ all_fields_read: true,
276
+ all_fields_write: true,
277
+ },
278
+ list_options: {},
279
+ },
280
+ });
281
+ return true;
282
+ });
283
+ });
253
284
  test('access-rule list executes against external root path', async () => {
254
285
  const out = await executeCLI('admin', ['access-rule', 'list', '--app-id', 'app_1', '--table-id', 'table_1'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
255
286
  assert.equal(out.kind, 'result');
@@ -396,6 +427,37 @@ test('row get resolves internal policy_id as query parameter', async () => {
396
427
  assert.equal(out.kind, 'result');
397
428
  assert.equal(calls[0], 'https://arcubase.example.com/api/entity/2188900691/2188900694/row/4000000046?policy_id=policy_hash_1');
398
429
  });
430
+ test('row get returns machine-readable row value evidence', async () => {
431
+ const out = await executeCLI('user', ['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'], env, withRowPolicyResolver('2188900694', ['view'], async () => new Response(JSON.stringify({
432
+ data: {
433
+ record: {
434
+ id: 4000000046,
435
+ fields: { 1001: '复测投票' },
436
+ },
437
+ fieldPermits: {
438
+ all_fields_read: false,
439
+ fields: [
440
+ { key: ':1001', read: true, write: true, report: false },
441
+ { key: ':1002', read: false, write: false, report: false },
442
+ { key: ':1003', read: true, write: true, report: false },
443
+ ],
444
+ },
445
+ },
446
+ }), { status: 200 })));
447
+ assert.deepEqual(out.rowValueEvidence, {
448
+ source: 'row get record.fields',
449
+ returnedFieldIds: ['1001'],
450
+ notReturnedFieldIds: ['1002', '1003'],
451
+ deniedReadFieldIds: ['1002'],
452
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
453
+ rules: [
454
+ 'current row values come only from row query fields or row get record.fields',
455
+ 'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
456
+ 'a field id absent from returned row fields has no row-value evidence',
457
+ 'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
458
+ ],
459
+ });
460
+ });
399
461
  test('row command rejects user-provided policy_id', async () => {
400
462
  const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' }, policy_id: 'manual_policy' });
401
463
  await assert.rejects(async () => {
@@ -81,6 +81,8 @@ test('admin access-rule help gives update whitelist and assign-users body-json e
81
81
  assert.equal(create.kind, 'help');
82
82
  assert.match(create.text, /Access rule/);
83
83
  assert.match(create.text, /access-rule\.md/);
84
+ assert.match(create.text, /full access/);
85
+ assert.match(create.text, /"actions":\["view","add","edit","delete"\]/);
84
86
  assert.match(create.text, /"user_scope":\[\{"type":"user","id":2188889901\}\]/);
85
87
  assert.doesNotMatch(create.text, /"user_scope":\[\{"type":"member"/);
86
88
  const update = await executeCLI('admin', ['access-rule', 'update', '--help'], env, async () => new Response('{}'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carthooks/arcubase-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Arcubase runtime CLI for admin and user command execution",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,6 +20,14 @@ Use the numeric `tenantUserId` / `TenantUser.ID`.
20
20
  Do not use hash ids such as `arcubaseTenantUserId`.
21
21
  Use `user_scope[].type:"user"` for a tenant user. Do not use FieldVO field type `member` in access rules.
22
22
 
23
+ ## Body Input
24
+
25
+ ```bash
26
+ --body-json '<json-string>' | --body-file access-rule.json
27
+ ```
28
+
29
+ Prefer `--body-json` for short payloads. `--body-file` is available for large or fragile JSON payloads.
30
+
23
31
  ## Create Read/Write Rule With Hidden Field
24
32
 
25
33
  ```bash
@@ -29,6 +37,17 @@ arcubase-admin access-rule create \
29
37
  --body-json '{"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":":1001","read":true,"write":true,"report":false},{"key":":1002","read":false,"write":false,"report":false},{"key":":1003","read":true,"write":true,"report":false}]},"list_options":{}}}'
30
38
  ```
31
39
 
40
+ ## Create Admin Full Access Rule
41
+
42
+ Use this for the operator's own admin entry before creating seed rows.
43
+
44
+ ```bash
45
+ arcubase-admin access-rule create \
46
+ --app-id 2188893436 \
47
+ --table-id 2188893443 \
48
+ --body-json '{"name":"Admin full access","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889977}],"policy":{"key":"custom","actions":["view","add","edit","delete"],"all_fields_read":true,"all_fields_write":true},"list_options":{}}}'
49
+ ```
50
+
32
51
  Rules:
33
52
 
34
53
  - read permission = `actions:["view"]`
@@ -83,6 +83,12 @@ Typical row shape:
83
83
  }
84
84
  ```
85
85
 
86
+ Returned values:
87
+
88
+ - current row values are only in `fields`
89
+ - fields absent from `fields` are not returned to the current user and have no row-value evidence
90
+ - table schema `fields[].value` and `default_value_mode` describe schema defaults, not current row values
91
+
86
92
  ## Read one row
87
93
 
88
94
  Command:
@@ -96,6 +102,13 @@ arcubase row get \
96
102
 
97
103
  Use this when you already know the row id and want the full record payload.
98
104
 
105
+ Returned values:
106
+
107
+ - current row values are in `record.fields`
108
+ - fields absent from `record.fields` are not returned to the current user and have no row-value evidence
109
+ - use `fieldPermits` only to explain access, not as current row values
110
+ - read visibility status is `fieldPermits read:false`; `FIELD_PERMISSION_REQUIRED` is a write preflight error from row create/update
111
+
99
112
  ## Update a row
100
113
 
101
114
  Command:
@@ -29,6 +29,7 @@ export type CLIErrorDetails = {
29
29
  responseBody?: string
30
30
  traceId?: string
31
31
  upstreamError?: unknown
32
+ retryToolCalls?: unknown[]
32
33
  suggestions?: string[]
33
34
  }
34
35
 
@@ -121,8 +121,9 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
121
121
  : []),
122
122
  ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'create'
123
123
  ? [
124
- 'body-json example:',
125
- ' - {"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":":1001","read":true,"write":true,"report":false},{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
124
+ 'body-json examples:',
125
+ ` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
126
+ ` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
126
127
  'success requires:',
127
128
  ' - result.enabled must be true',
128
129
  ' - options.user_scope[].type is "user" for tenant users',
@@ -567,6 +568,28 @@ function isAccessRuleUpdateCommand(scope: CommandScope, command: NonNullable<Ret
567
568
  }
568
569
 
569
570
  function buildAccessRuleShapeHint(userId = 2188889901) {
571
+ return buildHiddenFieldAccessRuleBody(userId)
572
+ }
573
+
574
+ function buildFullAccessRuleBody(userId = 2188889977) {
575
+ return {
576
+ name: 'Admin full access',
577
+ enabled: true,
578
+ type: 'form',
579
+ options: {
580
+ user_scope: [{ type: 'user', id: userId }],
581
+ policy: {
582
+ key: 'custom',
583
+ actions: ['view', 'add', 'edit', 'delete'],
584
+ all_fields_read: true,
585
+ all_fields_write: true,
586
+ },
587
+ list_options: {},
588
+ },
589
+ }
590
+ }
591
+
592
+ function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
570
593
  return {
571
594
  name: 'Sales read write',
572
595
  enabled: true,
@@ -585,6 +608,13 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
585
608
  }
586
609
  }
587
610
 
611
+ function buildAccessRuleCreateToolCall(body: unknown) {
612
+ return {
613
+ command: 'access-rule create',
614
+ args: ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json', JSON.stringify(body)],
615
+ }
616
+ }
617
+
588
618
  function buildAssignUsersValidationDetails(
589
619
  scope: CommandScope,
590
620
  command: NonNullable<ReturnType<typeof findCommand>>,
@@ -618,7 +648,7 @@ function buildAssignUsersValidationDetails(
618
648
  }
619
649
  }
620
650
 
621
- function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'shapeHint' | 'commonMistakes' | 'suggestions'> {
651
+ function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'retryToolCalls' | 'shapeHint' | 'commonMistakes' | 'suggestions'> {
622
652
  if (requestType === 'EntitySaveReqVO' || requestType === 'AppCreateEntityReqVO') {
623
653
  return {
624
654
  shapeHint: {
@@ -730,7 +760,15 @@ function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'shapeHin
730
760
  }
731
761
  }
732
762
  if (requestType === 'AppIngressCreateReqVO' || requestType === 'AppIngressUpdateReqVO') {
763
+ const retryToolCalls = [
764
+ buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
765
+ buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody()),
766
+ ]
733
767
  return {
768
+ retryToolCalls,
769
+ suggestions: [
770
+ 'next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access',
771
+ ],
734
772
  shapeHint: buildAccessRuleShapeHint(),
735
773
  commonMistakes: [
736
774
  'use options.policy, not top-level permissions',
@@ -1233,6 +1271,72 @@ function unwrapData(value: unknown): unknown {
1233
1271
  return current
1234
1272
  }
1235
1273
 
1274
+ function numericFieldKeys(fields: unknown): string[] {
1275
+ if (!isRecord(fields)) return []
1276
+ return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right))
1277
+ }
1278
+
1279
+ function permitFieldId(permit: unknown): string | undefined {
1280
+ if (!isRecord(permit) || typeof permit.key !== 'string') return undefined
1281
+ const match = /^:(\d+)$/.exec(permit.key)
1282
+ return match?.[1]
1283
+ }
1284
+
1285
+ function rowFieldPermits(value: unknown): Array<Record<string, unknown>> {
1286
+ const data = unwrapData(value)
1287
+ const fieldPermits = isRecord(data) && isRecord(data.fieldPermits) ? data.fieldPermits : undefined
1288
+ return fieldPermits && Array.isArray(fieldPermits.fields)
1289
+ ? fieldPermits.fields.filter((item): item is Record<string, unknown> => isRecord(item))
1290
+ : []
1291
+ }
1292
+
1293
+ function buildRowValueEvidence(command: NonNullable<ReturnType<typeof findCommand>>, responseBody: unknown): Record<string, unknown> | undefined {
1294
+ if (command.scope !== 'user' || command.commandPath[0] !== 'row') return undefined
1295
+ const action = command.commandPath[1]
1296
+ const data = unwrapData(responseBody)
1297
+ const rules = [
1298
+ 'current row values come only from row query fields or row get record.fields',
1299
+ 'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
1300
+ 'a field id absent from returned row fields has no row-value evidence',
1301
+ 'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
1302
+ ]
1303
+ if (action === 'get') {
1304
+ const record = isRecord(data) && isRecord(data.record) ? data.record : undefined
1305
+ const returnedFieldIds = numericFieldKeys(record?.fields)
1306
+ const permits = rowFieldPermits(responseBody)
1307
+ const permitFieldIds = permits.map(permitFieldId).filter((id): id is string => Boolean(id))
1308
+ const deniedReadFieldIds = permits
1309
+ .filter((permit) => permit.read === false)
1310
+ .map(permitFieldId)
1311
+ .filter((id): id is string => Boolean(id))
1312
+ .sort((left, right) => Number(left) - Number(right))
1313
+ const notReturnedFieldIds = permitFieldIds
1314
+ .filter((id) => !returnedFieldIds.includes(id))
1315
+ .sort((left, right) => Number(left) - Number(right))
1316
+ return {
1317
+ source: 'row get record.fields',
1318
+ returnedFieldIds,
1319
+ notReturnedFieldIds,
1320
+ deniedReadFieldIds,
1321
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
1322
+ rules,
1323
+ }
1324
+ }
1325
+ if (action === 'query') {
1326
+ const items = isRecord(data) && Array.isArray(data.items) ? data.items : []
1327
+ return {
1328
+ source: 'row query items[].fields',
1329
+ rows: items.filter((item): item is Record<string, unknown> => isRecord(item)).map((item) => ({
1330
+ rowId: item.id,
1331
+ returnedFieldIds: numericFieldKeys(item.fields),
1332
+ })),
1333
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
1334
+ rules,
1335
+ }
1336
+ }
1337
+ return undefined
1338
+ }
1339
+
1236
1340
  function requiredRowAction(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>): string | undefined {
1237
1341
  if (command.commandPath[0] !== 'row') return undefined
1238
1342
  switch (command.commandPath[1]) {
@@ -1632,7 +1736,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
1632
1736
  if (error instanceof CLIError && error.code === 'INVALID_BODY_JSON' && command.requestType) {
1633
1737
  throw new CLIError(
1634
1738
  'INVALID_BODY_JSON',
1635
- `body json is invalid for ${scope}.${command.commandPath.join(' ')}; fix JSON syntax, then use the canonical ${command.requestType} shape from docs/types`,
1739
+ `body json is invalid for ${scope}.${command.commandPath.join(' ')}; retry with retryToolCalls[0] or retryToolCalls[1] when present`,
1636
1740
  2,
1637
1741
  buildBodyValidationDetails(scope, command, command.requestType, [{ path: 'body', message: 'invalid JSON syntax' }], runtimeEnv),
1638
1742
  )
@@ -1782,10 +1886,12 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
1782
1886
  : {}),
1783
1887
  })
1784
1888
  }
1889
+ const rowValueEvidence = buildRowValueEvidence(command, parsedResponse)
1785
1890
  return {
1786
1891
  kind: 'result',
1787
1892
  commandPath: command.commandPath,
1788
1893
  status: response.status,
1789
1894
  data: parsedResponse,
1895
+ ...(rowValueEvidence ? { rowValueEvidence } : {}),
1790
1896
  }
1791
1897
  }
@@ -281,6 +281,38 @@ test('body json parse errors include command docs and type hints', async () => {
281
281
  })
282
282
  })
283
283
 
284
+ test('access-rule invalid json returns copyable create retry examples', async () => {
285
+ await assert.rejects(async () => {
286
+ await executeCLI('admin', ['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"Admin full access","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889977}],"policy":{"key":"custom","actions":["view","add","edit","delete"],"all_fields_read":true,"all_fields_write":true},"list_options":{}}'], env as any, async () => new Response('{}', { status: 200 }))
287
+ }, (error: unknown) => {
288
+ assert.ok(error instanceof CLIError)
289
+ assert.equal(error.code, 'INVALID_BODY_JSON')
290
+ assert.equal(error.details?.requestType, 'AppIngressCreateReqVO')
291
+ assert.match(error.message, /retryToolCalls/)
292
+ const toolCall = (error.details?.retryToolCalls ?? [])[0] as any
293
+ assert.ok(toolCall)
294
+ assert.equal(toolCall.command, 'access-rule create')
295
+ assert.deepEqual(toolCall.args.slice(0, 5), ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json'])
296
+ const body = JSON.parse(toolCall.args[5])
297
+ assert.deepEqual(body, {
298
+ name: 'Admin full access',
299
+ enabled: true,
300
+ type: 'form',
301
+ options: {
302
+ user_scope: [{ type: 'user', id: 2188889977 }],
303
+ policy: {
304
+ key: 'custom',
305
+ actions: ['view', 'add', 'edit', 'delete'],
306
+ all_fields_read: true,
307
+ all_fields_write: true,
308
+ },
309
+ list_options: {},
310
+ },
311
+ })
312
+ return true
313
+ })
314
+ })
315
+
284
316
  test('access-rule list executes against external root path', async () => {
285
317
  const out = await executeCLI(
286
318
  'admin',
@@ -477,6 +509,43 @@ test('row get resolves internal policy_id as query parameter', async () => {
477
509
  assert.equal(calls[0], 'https://arcubase.example.com/api/entity/2188900691/2188900694/row/4000000046?policy_id=policy_hash_1')
478
510
  })
479
511
 
512
+ test('row get returns machine-readable row value evidence', async () => {
513
+ const out = await executeCLI(
514
+ 'user',
515
+ ['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'],
516
+ env as any,
517
+ withRowPolicyResolver('2188900694', ['view'], async () => new Response(JSON.stringify({
518
+ data: {
519
+ record: {
520
+ id: 4000000046,
521
+ fields: { 1001: '复测投票' },
522
+ },
523
+ fieldPermits: {
524
+ all_fields_read: false,
525
+ fields: [
526
+ { key: ':1001', read: true, write: true, report: false },
527
+ { key: ':1002', read: false, write: false, report: false },
528
+ { key: ':1003', read: true, write: true, report: false },
529
+ ],
530
+ },
531
+ },
532
+ }), { status: 200 })),
533
+ )
534
+ assert.deepEqual(out.rowValueEvidence, {
535
+ source: 'row get record.fields',
536
+ returnedFieldIds: ['1001'],
537
+ notReturnedFieldIds: ['1002', '1003'],
538
+ deniedReadFieldIds: ['1002'],
539
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
540
+ rules: [
541
+ 'current row values come only from row query fields or row get record.fields',
542
+ 'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
543
+ 'a field id absent from returned row fields has no row-value evidence',
544
+ 'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
545
+ ],
546
+ })
547
+ })
548
+
480
549
  test('row command rejects user-provided policy_id', async () => {
481
550
  const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' }, policy_id: 'manual_policy' })
482
551
  await assert.rejects(async () => {
@@ -90,6 +90,8 @@ test('admin access-rule help gives update whitelist and assign-users body-json e
90
90
  assert.equal(create.kind, 'help')
91
91
  assert.match(create.text, /Access rule/)
92
92
  assert.match(create.text, /access-rule\.md/)
93
+ assert.match(create.text, /full access/)
94
+ assert.match(create.text, /"actions":\["view","add","edit","delete"\]/)
93
95
  assert.match(create.text, /"user_scope":\[\{"type":"user","id":2188889901\}\]/)
94
96
  assert.doesNotMatch(create.text, /"user_scope":\[\{"type":"member"/)
95
97