@akanjs/cli 3.0.0-alpha.15 → 3.0.0-alpha.17
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/.build-stamp +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/{abstractCompactor-e5yq60qz.js → abstractCompactor-khr60389.js} +1 -1
- package/{agent.command-h6tc0pzh.js → agent.command-e835fjem.js} +6 -6
- package/{application.command-kphnfwyg.js → application.command-bexm4597.js} +5 -5
- package/{applicationBuildRunner-9ps10a0h.js → applicationBuildRunner-sgr3aq5b.js} +5 -6
- package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-93nzdfsy.js} +1 -1
- package/buildBatch.proc.js +4 -4
- package/{capacitorApp-9ejk3k7q.js → capacitorApp-qv7g3dp2.js} +27 -27
- package/{cloud.command-zn6mfscb.js → cloud.command-v44fdp23.js} +8 -8
- package/{context.command-pkbccyys.js → context.command-q8r0tge3.js} +13 -13
- package/{dependencyScanner-f6grrep3.js → dependencyScanner-rbs2407b.js} +1 -1
- package/{getDirname-m9s0cmhq.js → getDirname-ka11hm7e.js} +1 -1
- package/{guideline.command-0jj2k77g.js → guideline.command-pye36g73.js} +3 -3
- package/guidelines/conventions/conventions.instruction.md +122 -43
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +1 -3
- package/incrementalBuilder.proc.js +3 -3
- package/{index-xwfkgss0.js → index-33zce8gt.js} +8 -4
- package/{index-w7farjxd.js → index-3dp7x6rc.js} +3 -3
- package/{index-p42te6qb.js → index-73rnbwgg.js} +14 -6
- package/{index-1twehz1x.js → index-8vkkm7v0.js} +8 -18
- package/{index-g3brq942.js → index-9djnsdgf.js} +5 -5
- package/{index-b9m84bjp.js → index-d4d3q2r8.js} +10 -10
- package/{index-96vpa123.js → index-fb2wa7j3.js} +3 -3
- package/{index-djh5gpb7.js → index-g9e2fhw2.js} +2 -2
- package/{index-4v0wvt37.js → index-hxp90q94.js} +4 -4
- package/{index-7ecpft60.js → index-k6fvbcj1.js} +3 -3
- package/{index-aak9cctp.js → index-myy6xy5w.js} +4 -4
- package/{index-gz91pc3p.js → index-mz5jtaq7.js} +1 -1
- package/{index-bkwnr08k.js → index-n706txcv.js} +2 -2
- package/{index-gyvrhtpm.js → index-qc0tnhs2.js} +1 -1
- package/{index-j9cxndc0.js → index-qyffprfs.js} +7 -5
- package/{index-8d9r0df5.js → index-sn71brf9.js} +2 -2
- package/{index-9s991k9v.js → index-tsmjxph2.js} +1 -1
- package/{index-2m11t9ek.js → index-v5vmgh0z.js} +3 -208
- package/{index-rv16ck4r.js → index-x2w0y8sz.js} +8 -8
- package/{index-4agbdds8.js → index-xyrb7pk5.js} +3 -3
- package/index.js +19 -19
- package/{library.command-wbptp6q3.js → library.command-v5cyaw9t.js} +4 -4
- package/{localRegistry.command-6ea3pgq0.js → localRegistry.command-tqnh5g77.js} +7 -7
- package/{module.command-ghkf919z.js → module.command-wxsvyadc.js} +6 -6
- package/{package.command-ph2m34tv.js → package.command-qkcs60ts.js} +4 -4
- package/package.json +3 -4
- package/{page.command-atnzfn88.js → page.command-qj5bfnh9.js} +4 -4
- package/{primitive.command-7qbdhfc2.js → primitive.command-c5jtk3e9.js} +7 -7
- package/{quality.command-1ff2xcra.js → quality.command-f8drhx6c.js} +91 -362
- package/{recipeScanner-tghkzrrt.js → recipeScanner-zrzev25x.js} +3 -3
- package/{repair.command-weakn0yr.js → repair.command-mzwtejpk.js} +6 -6
- package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-7fycdqyv.js} +1 -1
- package/{scalar.command-xdjhvsgb.js → scalar.command-0yvwfv8m.js} +5 -5
- package/templates/appSample/srvkit/AuthGuard.ts +2 -2
- package/{typeChecker-kravn7ns.js → typeChecker-g6bh0xfv.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/{workflow.command-msm2tjee.js → workflow.command-ak6at7nz.js} +10 -10
- package/{workspace.command-gr8z82z9.js → workspace.command-9ncr56g4.js} +19 -20
- /package/{index-r24hmh0q.js → index-yb2wsc7f.js} +0 -0
package/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
04e275c86b39878b8f59c901ec9b2e94fe61c317e6279d0f7b890998dc0fb6f4
|
package/README.ko.md
CHANGED
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
AgentScript
|
|
4
|
-
} from "./index-
|
|
5
|
-
import"./index-
|
|
6
|
-
import"./index-
|
|
4
|
+
} from "./index-k6fvbcj1.js";
|
|
5
|
+
import"./index-g9e2fhw2.js";
|
|
6
|
+
import"./index-tsmjxph2.js";
|
|
7
7
|
import"./index-j37qq1f2.js";
|
|
8
8
|
import {
|
|
9
9
|
Workspace,
|
|
10
10
|
command
|
|
11
|
-
} from "./index-
|
|
12
|
-
import"./index-
|
|
11
|
+
} from "./index-n706txcv.js";
|
|
12
|
+
import"./index-8vkkm7v0.js";
|
|
13
13
|
import"./index-mxvakhsm.js";
|
|
14
14
|
import"./index-xys926f2.js";
|
|
15
15
|
import"./index-1577bej2.js";
|
|
16
16
|
import"./index-67546d0j.js";
|
|
17
|
-
import"./index-
|
|
17
|
+
import"./index-yb2wsc7f.js";
|
|
18
18
|
|
|
19
19
|
// pkgs/@akanjs/cli/agent/agent.command.ts
|
|
20
20
|
class AgentCommand extends command("agent", [AgentScript], ({ public: target }) => ({
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
} from "./index-0wae5ebk.js";
|
|
5
5
|
import {
|
|
6
6
|
ApplicationScript
|
|
7
|
-
} from "./index-
|
|
8
|
-
import"./index-
|
|
7
|
+
} from "./index-x2w0y8sz.js";
|
|
8
|
+
import"./index-73rnbwgg.js";
|
|
9
9
|
import {
|
|
10
10
|
getMobileTargetChoices
|
|
11
11
|
} from "./index-76rn3g2c.js";
|
|
@@ -15,14 +15,14 @@ import {
|
|
|
15
15
|
Sys,
|
|
16
16
|
Workspace,
|
|
17
17
|
command
|
|
18
|
-
} from "./index-
|
|
18
|
+
} from "./index-n706txcv.js";
|
|
19
19
|
import"./index-fgc8r6dj.js";
|
|
20
20
|
import"./index-bjpxzr6s.js";
|
|
21
|
-
import"./index-
|
|
21
|
+
import"./index-8vkkm7v0.js";
|
|
22
22
|
import"./index-mxvakhsm.js";
|
|
23
23
|
import"./index-1577bej2.js";
|
|
24
24
|
import"./index-67546d0j.js";
|
|
25
|
-
import"./index-
|
|
25
|
+
import"./index-yb2wsc7f.js";
|
|
26
26
|
|
|
27
27
|
// pkgs/@akanjs/cli/application/application.command.ts
|
|
28
28
|
import { select } from "@inquirer/prompts";
|
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
CsrArtifactBuilder,
|
|
5
5
|
SsrBaseArtifactBuilder,
|
|
6
6
|
precompressArtifacts
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-v5vmgh0z.js";
|
|
8
8
|
import"./index-xjd3rqzj.js";
|
|
9
|
-
import"./index-
|
|
9
|
+
import"./index-qyffprfs.js";
|
|
10
10
|
import"./index-bjpxzr6s.js";
|
|
11
11
|
import {
|
|
12
12
|
Spinner
|
|
13
13
|
} from "./index-mxvakhsm.js";
|
|
14
|
-
import"./index-
|
|
14
|
+
import"./index-yb2wsc7f.js";
|
|
15
15
|
|
|
16
16
|
// pkgs/@akanjs/devkit/applicationBuildRunner.ts
|
|
17
17
|
import { mkdir, rm } from "fs/promises";
|
|
@@ -39,7 +39,6 @@ var SSR_RENDER_EXTERNALS = [
|
|
|
39
39
|
var AKAN_OPTIONAL_BACKEND_EXTERNALS = [
|
|
40
40
|
"@libsql/client",
|
|
41
41
|
"bullmq",
|
|
42
|
-
"croner",
|
|
43
42
|
"ioredis",
|
|
44
43
|
"postgres",
|
|
45
44
|
"protobufjs"
|
|
@@ -287,6 +286,6 @@ void run().catch((error) => {
|
|
|
287
286
|
}
|
|
288
287
|
}
|
|
289
288
|
export {
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
AKAN_OPTIONAL_BACKEND_EXTERNALS,
|
|
290
|
+
ApplicationBuildRunner
|
|
292
291
|
};
|
package/buildBatch.proc.js
CHANGED
|
@@ -5,18 +5,18 @@ import {
|
|
|
5
5
|
FontOptimizer,
|
|
6
6
|
PagesBundleBuilder,
|
|
7
7
|
SsrBaseArtifactBuilder
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-v5vmgh0z.js";
|
|
9
9
|
import"./index-xjd3rqzj.js";
|
|
10
|
-
import"./index-
|
|
10
|
+
import"./index-qyffprfs.js";
|
|
11
11
|
import"./index-bjpxzr6s.js";
|
|
12
12
|
import {
|
|
13
13
|
AppExecutor,
|
|
14
14
|
WorkspaceExecutor
|
|
15
|
-
} from "./index-
|
|
15
|
+
} from "./index-8vkkm7v0.js";
|
|
16
16
|
import"./index-mxvakhsm.js";
|
|
17
17
|
import"./index-1577bej2.js";
|
|
18
18
|
import"./index-67546d0j.js";
|
|
19
|
-
import"./index-
|
|
19
|
+
import"./index-yb2wsc7f.js";
|
|
20
20
|
|
|
21
21
|
// pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts
|
|
22
22
|
import path from "path";
|
|
@@ -25,37 +25,37 @@ import {
|
|
|
25
25
|
selectLocalDevHost,
|
|
26
26
|
sortIosRunTargets,
|
|
27
27
|
writeRootCapacitorConfig
|
|
28
|
-
} from "./index-
|
|
28
|
+
} from "./index-mz5jtaq7.js";
|
|
29
29
|
import"./index-76rn3g2c.js";
|
|
30
|
-
import"./index-
|
|
30
|
+
import"./index-8vkkm7v0.js";
|
|
31
31
|
import"./index-mxvakhsm.js";
|
|
32
32
|
import"./index-1577bej2.js";
|
|
33
33
|
import"./index-67546d0j.js";
|
|
34
|
-
import"./index-
|
|
34
|
+
import"./index-yb2wsc7f.js";
|
|
35
35
|
export {
|
|
36
|
-
|
|
37
|
-
sortIosRunTargets,
|
|
38
|
-
selectLocalDevHost,
|
|
39
|
-
sanitizeIosNativeRunEnv,
|
|
40
|
-
rootCapacitorConfigPaths,
|
|
41
|
-
rootCapacitorConfigFilenames,
|
|
42
|
-
raiseGradleMinSdkVersion,
|
|
43
|
-
parseSimctlDevices,
|
|
44
|
-
parseIosRuntimeMajor,
|
|
45
|
-
parseDevicectlDevices,
|
|
46
|
-
materializeCapacitorConfig,
|
|
47
|
-
isPlaceholderAppId,
|
|
48
|
-
getMissingAndroidReleaseSigningKeys,
|
|
49
|
-
getAndroidLocalServerHost,
|
|
50
|
-
getAdbDeviceStateIssues,
|
|
51
|
-
formatIosRunFailureMessage,
|
|
52
|
-
formatAndroidReleaseSigningError,
|
|
53
|
-
clearRootCapacitorConfigs,
|
|
54
|
-
classifyIosRunFailure,
|
|
55
|
-
buildIosNativeRunCommand,
|
|
56
|
-
assertJsonSerializable,
|
|
57
|
-
SWIFTUICORE_MIN_IOS_MAJOR,
|
|
58
|
-
PLACEHOLDER_APP_IDS,
|
|
36
|
+
ANDROID_MIN_SDK_VERSION,
|
|
59
37
|
CapacitorApp,
|
|
60
|
-
|
|
38
|
+
PLACEHOLDER_APP_IDS,
|
|
39
|
+
SWIFTUICORE_MIN_IOS_MAJOR,
|
|
40
|
+
assertJsonSerializable,
|
|
41
|
+
buildIosNativeRunCommand,
|
|
42
|
+
classifyIosRunFailure,
|
|
43
|
+
clearRootCapacitorConfigs,
|
|
44
|
+
formatAndroidReleaseSigningError,
|
|
45
|
+
formatIosRunFailureMessage,
|
|
46
|
+
getAdbDeviceStateIssues,
|
|
47
|
+
getAndroidLocalServerHost,
|
|
48
|
+
getMissingAndroidReleaseSigningKeys,
|
|
49
|
+
isPlaceholderAppId,
|
|
50
|
+
materializeCapacitorConfig,
|
|
51
|
+
parseDevicectlDevices,
|
|
52
|
+
parseIosRuntimeMajor,
|
|
53
|
+
parseSimctlDevices,
|
|
54
|
+
raiseGradleMinSdkVersion,
|
|
55
|
+
rootCapacitorConfigFilenames,
|
|
56
|
+
rootCapacitorConfigPaths,
|
|
57
|
+
sanitizeIosNativeRunEnv,
|
|
58
|
+
selectLocalDevHost,
|
|
59
|
+
sortIosRunTargets,
|
|
60
|
+
writeRootCapacitorConfig
|
|
61
61
|
};
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
CloudScript
|
|
4
|
-
} from "./index-
|
|
5
|
-
import"./index-
|
|
6
|
-
import"./index-
|
|
4
|
+
} from "./index-9djnsdgf.js";
|
|
5
|
+
import"./index-fb2wa7j3.js";
|
|
6
|
+
import"./index-33zce8gt.js";
|
|
7
7
|
import {
|
|
8
8
|
GlobalConfig
|
|
9
9
|
} from "./index-0cj2zxbm.js";
|
|
10
|
-
import"./index-
|
|
11
|
-
import"./index-
|
|
10
|
+
import"./index-x2w0y8sz.js";
|
|
11
|
+
import"./index-73rnbwgg.js";
|
|
12
12
|
import"./index-76rn3g2c.js";
|
|
13
13
|
import {
|
|
14
14
|
Workspace,
|
|
15
15
|
command
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-n706txcv.js";
|
|
17
17
|
import"./index-fgc8r6dj.js";
|
|
18
18
|
import"./index-bjpxzr6s.js";
|
|
19
|
-
import"./index-
|
|
19
|
+
import"./index-8vkkm7v0.js";
|
|
20
20
|
import"./index-mxvakhsm.js";
|
|
21
21
|
import"./index-1577bej2.js";
|
|
22
22
|
import"./index-46tjzh6s.js";
|
|
23
23
|
import"./index-67546d0j.js";
|
|
24
|
-
import"./index-
|
|
24
|
+
import"./index-yb2wsc7f.js";
|
|
25
25
|
|
|
26
26
|
// pkgs/@akanjs/cli/cloud/cloud.command.ts
|
|
27
27
|
var localRegistryUrl = () => process.env.AKAN_NPM_REGISTRY ?? "http://127.0.0.1:4873";
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
ContextScript
|
|
4
|
-
} from "./index-
|
|
5
|
-
import"./index-
|
|
6
|
-
import"./index-
|
|
7
|
-
import"./index-
|
|
8
|
-
import"./index-
|
|
9
|
-
import"./index-
|
|
4
|
+
} from "./index-d4d3q2r8.js";
|
|
5
|
+
import"./index-xyrb7pk5.js";
|
|
6
|
+
import"./index-3dp7x6rc.js";
|
|
7
|
+
import"./index-sn71brf9.js";
|
|
8
|
+
import"./index-hxp90q94.js";
|
|
9
|
+
import"./index-myy6xy5w.js";
|
|
10
10
|
import"./index-ss469dec.js";
|
|
11
|
-
import"./index-
|
|
12
|
-
import"./index-
|
|
11
|
+
import"./index-qc0tnhs2.js";
|
|
12
|
+
import"./index-mz5jtaq7.js";
|
|
13
13
|
import"./index-0cj2zxbm.js";
|
|
14
|
-
import"./index-
|
|
15
|
-
import"./index-
|
|
14
|
+
import"./index-g9e2fhw2.js";
|
|
15
|
+
import"./index-tsmjxph2.js";
|
|
16
16
|
import"./index-j37qq1f2.js";
|
|
17
17
|
import"./index-76rn3g2c.js";
|
|
18
18
|
import {
|
|
19
19
|
Workspace,
|
|
20
20
|
command
|
|
21
|
-
} from "./index-
|
|
22
|
-
import"./index-
|
|
21
|
+
} from "./index-n706txcv.js";
|
|
22
|
+
import"./index-8vkkm7v0.js";
|
|
23
23
|
import"./index-mxvakhsm.js";
|
|
24
24
|
import"./index-xys926f2.js";
|
|
25
25
|
import"./index-1577bej2.js";
|
|
26
26
|
import"./index-67546d0j.js";
|
|
27
|
-
import"./index-
|
|
27
|
+
import"./index-yb2wsc7f.js";
|
|
28
28
|
|
|
29
29
|
// pkgs/@akanjs/cli/context/context.command.ts
|
|
30
30
|
class ContextCommand extends command("context", [ContextScript], ({ public: target }) => ({
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
command,
|
|
11
11
|
runner,
|
|
12
12
|
script
|
|
13
|
-
} from "./index-
|
|
14
|
-
import"./index-
|
|
13
|
+
} from "./index-n706txcv.js";
|
|
14
|
+
import"./index-8vkkm7v0.js";
|
|
15
15
|
import"./index-mxvakhsm.js";
|
|
16
16
|
import {
|
|
17
17
|
getDirname
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
FileSys
|
|
21
21
|
} from "./index-67546d0j.js";
|
|
22
|
-
import"./index-
|
|
22
|
+
import"./index-yb2wsc7f.js";
|
|
23
23
|
|
|
24
24
|
// pkgs/@akanjs/cli/guideline/guideline.script.ts
|
|
25
25
|
import { Logger } from "akanjs/common";
|
|
@@ -57,6 +57,12 @@ that looks wrong; do not "fix" it back.
|
|
|
57
57
|
unreachable — write it into state with `this.set({ ... })`. A bare `return;` guard, a `return` inside a nested
|
|
58
58
|
callback, a getter, and a `static` helper are all still fine.
|
|
59
59
|
- **Never redeclare a generated CRUD endpoint name** in `*.signal.ts` (`no-redeclare-predefined-endpoint.grit`).
|
|
60
|
+
- **Never type a `*.Util.tsx` / `*.Zone.tsx` prop as a `cnst` model** (`no-model-type-in-util-zone.grit`). Those two
|
|
61
|
+
roles are always client components, so a `cnst.Banner` / `cnst.LightBanner` prop is a class instance the server has
|
|
62
|
+
to hand across the boundary; take `bannerId: string` and read the model from the store instead. Two shapes are
|
|
63
|
+
exempt because neither is an instance: `cnst.<Enum>["value"]`, whose indexed access resolves to a string union, and
|
|
64
|
+
a `ClientInit` / `ClientView` / `ClientEdit` type argument, which the framework maps to `GetStateObject<…>` plain
|
|
65
|
+
data. Any *other* indexed access is still flagged — `cnst.Banner["image"]` is a `File`.
|
|
60
66
|
- **No deep imports past a barrel** (`no-deep-internal-import.grit`). Cross-module constant references such as
|
|
61
67
|
`../map/map.constant` are the sanctioned exception.
|
|
62
68
|
- **Never import across the client/server boundary.** Client files (`ui/`, `webkit/`, `page/`, `*.store.ts`, every
|
|
@@ -344,10 +350,6 @@ than returning it (`no-return-in-store-action.grit`); a bare `return;` guard sta
|
|
|
344
350
|
`.of() → .model() → .insight() → .query() → .sort() → .enum() → .slice() → .endpoint() → .error() → .translate()`.
|
|
345
351
|
Name every argument in `.arg()`, including framework-supplied `skip` / `limit` / `sort`. Use `modelDictionary`,
|
|
346
352
|
`scalarDictionary`, or `serviceDictionary` to match the module kind.
|
|
347
|
-
**`.store()` sits between `.endpoint()` and `.error()` and is the one optional stage** — omit it entirely rather
|
|
348
|
-
than writing it empty. It names custom store actions (labels and `.desc()` only, no `.arg()`), and it is only
|
|
349
|
-
needed where inheriting would be wrong: an action named after the endpoint it calls already reads as that
|
|
350
|
-
endpoint's `.desc()`, which is most of them. `akan.agent.missing-store-description` names the rest.
|
|
351
353
|
|
|
352
354
|
**`<module>.abstract.md`** — a title line, one declarative sentence naming what the module owns, a `## Rules` list of
|
|
353
355
|
two to five invariants the code cannot show, and an optional workflow arrow chain
|
|
@@ -374,8 +376,10 @@ workflow changes.
|
|
|
374
376
|
|
|
375
377
|
- **Every `slice()` takes an explicit `{ guards: {…} }` second argument, and `root:` is always `Admin`.**
|
|
376
378
|
- **Every custom `mutation` / `query` / `message` names its own `guards: [...]` array.** Never rely on the slice default. `Public` belongs on a slice `get:`, never on a mutation.
|
|
379
|
+
- **The guards are also the MCP exposure decision** — see MCP Exposure. An endpoint that names none is not published to agents at all, and a mutation whose only guard is `Public` is refused, so a missing `guards` array now costs visibility as well as authorization.
|
|
377
380
|
- Resource guards are `Can<Verb><Model>` classes in `srvkit/guards.ts` that `implements Guard` with an `async canPass(context)`. They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn` then `false`. Admin bypass goes first.
|
|
378
381
|
- Keep `static name = "User";` on guard classes. `fetch` serializes guard names and the API explorer filters on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
|
|
382
|
+
- **Every guard class also declares `static scope: GuardScope`, and it is required with no default.** `"account"` means the verdict reads the caller and nothing about the call, so it can be evaluated with no arguments — which is what lets an MCP listing hide what this caller certainly cannot use. `"resource"` means it needs the call's arguments (`context.getArg()`) and fails closed without them, so it is never evaluated for a listing: the entry stays visible and is stopped at call time. Getting it wrong is not a type error, so the marker is mandatory rather than defaulted — `SignedIn` / `Admin` / role checks are `"account"`, and every `Can<Verb><Model>` is `"resource"`.
|
|
379
383
|
- The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
|
|
380
384
|
- Guards ship with the library that owns the model and are imported by its own signals through the package path, so a mounting app inherits authorization and cannot forget it.
|
|
381
385
|
- Services re-check ownership even when a guard already gated the call — two independent gates.
|
|
@@ -466,7 +470,7 @@ Conventions that hold for both shapes:
|
|
|
466
470
|
- Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at module scope.
|
|
467
471
|
- Extend a function by appending an optional trailing parameter with a default, never by changing arity.
|
|
468
472
|
- Parameters: up to three required primitives positional; optional flags in a trailing `{ … } = {}`; four or more parameters, or any two same-typed strings, in one named destructured object.
|
|
469
|
-
- Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`
|
|
473
|
+
- Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise (`puppeteerLoad ??= import("puppeteer")`).
|
|
470
474
|
|
|
471
475
|
### Error Placement
|
|
472
476
|
|
|
@@ -477,39 +481,56 @@ Conventions that hold for both shapes:
|
|
|
477
481
|
|
|
478
482
|
### MCP Exposure
|
|
479
483
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
Every signal is served to AI agents as an MCP server on `POST /mcp`. **`/mcp` is mounted by default and exposure
|
|
485
|
+
follows an endpoint's guards — there is no per-endpoint opt-in, and nothing to write in a signal file.** An endpoint
|
|
486
|
+
that declares a real guard is published; one that declares none is refused, and so is a mutation whose only guard is
|
|
487
|
+
`Public`. `AKAN_MCP=false` takes the whole surface off. The reasoning is that the guards are already the
|
|
488
|
+
authorization decision and `filterForAccount` re-reads them per caller on every listing, so a second per-endpoint
|
|
489
|
+
switch says nothing the guards do not — while guaranteeing that every endpoint added later is invisible to agents
|
|
490
|
+
until somebody remembers it.
|
|
491
|
+
|
|
492
|
+
Settings live in the app's `lib/option.ts` — `option.setMcp({ … })`, taking `enabled`, `readOnly`, `path`,
|
|
493
|
+
`version`, `instructions`, `allowedOrigins`, `pageSize`, `language`, and `auth`. **Not `main.ts`**: the gateway
|
|
494
|
+
there only spawns children, and `option.ts` is the app-authored file `server.ts` already hands to the process that
|
|
495
|
+
mounts `/mcp`. Every lib's option is read in mount order with the app's last, so an app tightens what a library
|
|
496
|
+
declared without restating it. Each field also has an env spelling (`AKAN_MCP`, `AKAN_MCP_READONLY`,
|
|
485
497
|
`AKAN_MCP_PATH`, `AKAN_MCP_VERSION`, `AKAN_MCP_INSTRUCTIONS`, `AKAN_MCP_ALLOWED_ORIGINS`, `AKAN_MCP_PAGE_SIZE`,
|
|
486
|
-
`AKAN_MCP_LANGUAGE`, `AKAN_MCP_AUTH_SERVERS`, `AKAN_MCP_SCOPES`, `AKAN_MCP_RESOURCE`)
|
|
498
|
+
`AKAN_MCP_LANGUAGE`, `AKAN_MCP_AUTH_SERVERS`, `AKAN_MCP_SCOPES`, `AKAN_MCP_RESOURCE`) for a deployment that
|
|
499
|
+
configures what the source does not, which the option overrides.
|
|
487
500
|
The two booleans answer to `AKAN_PUBLIC_MCP` / `AKAN_PUBLIC_MCP_READONLY` too, the same pairing `AKAN_OPENAPI`
|
|
488
501
|
has, and a value written in code wins over the env of the same name — an explicit `undefined` is not a value.
|
|
489
502
|
`AKAN_MCP_PATH` is normalized to a leading `/`, because the route key and the OAuth metadata path are both built by
|
|
490
503
|
concatenation.
|
|
491
504
|
|
|
492
505
|
```typescript
|
|
493
|
-
//
|
|
506
|
+
// apps/<app>/lib/option.ts
|
|
507
|
+
export const option = new AkanOption<ModulesOptions>().setMcp({
|
|
508
|
+
instructions: "Domain tools for the akan app. Start from taskInTodo.",
|
|
509
|
+
language: "en",
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// <model>.signal.ts — every one of these is an MCP tool or prompt, with no `mcp:` option anywhere
|
|
494
515
|
export class TaskSlice extends slice(
|
|
495
516
|
srv.task,
|
|
496
|
-
|
|
497
|
-
{ guards: { root: Admin, get: SignedIn, cru: SignedIn }, mcp: { get: true, list: true } },
|
|
517
|
+
{ guards: { root: Admin, get: SignedIn, cru: SignedIn } },
|
|
498
518
|
(init) => ({
|
|
499
|
-
// its own guards: the map above reaches base CRUD and the root slice, never a named slice
|
|
500
|
-
|
|
519
|
+
// its own guards: the map above reaches base CRUD and the root slice, never a named slice — so a named slice
|
|
520
|
+
// that names none is refused rather than published, which is the one shape to watch for.
|
|
521
|
+
inTodo: init({ guards: [SignedIn] }).exec(function () {
|
|
501
522
|
return this.taskService.queryByStatuses(["todo"]);
|
|
502
523
|
}),
|
|
503
524
|
}),
|
|
504
525
|
) {}
|
|
505
526
|
|
|
506
527
|
export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => ({
|
|
507
|
-
startTask: mutation(cnst.Task, { guards: [SignedIn]
|
|
528
|
+
startTask: mutation(cnst.Task, { guards: [SignedIn] })
|
|
508
529
|
.param("taskId", ID)
|
|
509
530
|
.exec(async function (taskId) {
|
|
510
531
|
return await this.taskService.startTask(taskId);
|
|
511
532
|
}),
|
|
512
|
-
reviewTask: prompt({ guards: [SignedIn]
|
|
533
|
+
reviewTask: prompt({ guards: [SignedIn] })
|
|
513
534
|
.param("taskId", ID)
|
|
514
535
|
.exec(async function (taskId) {
|
|
515
536
|
const task = await this.taskService.getTask(taskId);
|
|
@@ -518,24 +539,21 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
518
539
|
})) {}
|
|
519
540
|
```
|
|
520
541
|
|
|
521
|
-
- **The refusals are fail-closed
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
must be filled**.
|
|
542
|
+
- **The refusals are fail-closed**: **an endpoint that declares no `guards` at all** (nobody decided who may reach
|
|
543
|
+
it), **a mutation with no real `guards`** (`[Public]` is having none, spelled out — it answers true
|
|
544
|
+
unconditionally), `pubsub` and `message` (their internal args read a socket an MCP request does not have), an
|
|
545
|
+
`Any` or `Upload` return, a file upload, and **an argument typed `Any` that must be filled**.
|
|
525
546
|
A `prompt` refuses two more, because its `arguments` is one string per name with no schema beside it: a **list
|
|
526
547
|
argument**, which could never carry a second value, and **any `Any` argument** — a tool leaves that out of its
|
|
527
|
-
schema, and a prompt has no schema to leave it out of.
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
fail-closed is right, and a silent fail-closed leaves you nothing to read. `akan quality scan` covers the two
|
|
533
|
-
shapes visible in source, `akan.mcp.missing-description` and `akan.mcp.unguarded-exposure`; the API explorer
|
|
534
|
-
badges the per-endpoint rules (`MCP` / `MCP refused`) from the same rule the catalogue runs.
|
|
548
|
+
schema, and a prompt has no schema to leave it out of.
|
|
549
|
+
- **Every refusal is named in the boot log**: one `warn` per endpoint plus a `MCP catalogue: tools=… prompts=…`
|
|
550
|
+
count. Read that line first when a tool you expected is missing — and it is the *only* place the answer exists,
|
|
551
|
+
because there is no absent opt-in to notice. The API explorer badges the same rule per endpoint (`MCP` /
|
|
552
|
+
`MCP refused`), from the same shared implementation the catalogue runs.
|
|
535
553
|
- **An `Any` argument is left out of the published schema** rather than described as `{}` — it tells a model
|
|
536
554
|
nothing — and a value sent for one is refused by name, so the endpoint reads it as omitted. That is what happens
|
|
537
555
|
to the root list's raw `query` descriptor: read as sent, it would be an arbitrary filter over every model you
|
|
538
|
-
|
|
556
|
+
publish. Declare a named filter slice when an agent should narrow a list.
|
|
539
557
|
- **A nullable model return publishes no `outputSchema`**, and its empty answer ships as the text `null` with no
|
|
540
558
|
`structuredContent`. That field is an object by definition, so `null` cannot ride in it any more than an array
|
|
541
559
|
can — a list is wrapped as `{ items: … }` for the same reason — and a declared schema obliges every result to
|
|
@@ -544,13 +562,13 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
544
562
|
- **An `outputSchema` names no `hidden` or `secret` field.** Every response has both stripped, so publishing them
|
|
545
563
|
promises a property no answer can carry — and on a model like `user` the names are the leak. Your *input* schema
|
|
546
564
|
keeps them: they are legal to send, and the same model describes a request body.
|
|
547
|
-
-
|
|
565
|
+
- A refused endpoint answers the *same* "unknown tool" as one that does not exist. Never make that
|
|
548
566
|
message more helpful — the difference is what enumerates your private surface. A guard's refusal is generalized
|
|
549
567
|
the same way: the caller reads `You are not permitted to perform this action.`, never `Access denied by guard:
|
|
550
568
|
Admin`, which names your authorization structure to the one caller barred from it. A domain `Err` resolves
|
|
551
569
|
through the dictionary first and keeps its own words.
|
|
552
|
-
- `
|
|
553
|
-
distrust hints; they are never a gate.
|
|
570
|
+
- The `readOnly` / `destructive` / `idempotent` hints a client renders are derived from the endpoint type and key
|
|
571
|
+
and are not configurable. Clients are told to distrust hints; they are never a gate.
|
|
554
572
|
- **`AKAN_MCP_READONLY=true` is the read-only-deployment valve, not the exposure switch.** It drops every mutation
|
|
555
573
|
whatever it declared, and reports each one in the boot log like any other refusal.
|
|
556
574
|
- OAuth resource metadata is published at `/.well-known/oauth-protected-resource` (and at that path plus the mount
|
|
@@ -558,12 +576,12 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
558
576
|
configure it; `insufficient_scope` is enforced only once `AKAN_MCP_SCOPES` is set. A token carrying no `aud` at
|
|
559
577
|
all is refused once `AKAN_MCP_AUTH_SERVERS` names an issuer — that issuer mints tokens for its other resources
|
|
560
578
|
too — and accepted while none is named, because a first-party Akan token is bound by app and environment.
|
|
561
|
-
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
579
|
+
- **The boot log names every published entry with no dictionary `.desc()`.** An agent picks a tool by its
|
|
580
|
+
description, so a missing one is a broken tool. What the framework generates has no text of its own and borrows
|
|
581
|
+
the model's: the generated list reads the `.of()` label, and the base CRUD tools append the model's `.desc()` to
|
|
582
|
+
their generated `Get X`. Write that model `.desc()` — it is the only text those entries can carry. There is no
|
|
583
|
+
`akan quality scan` rule for this any more: a source scanner found the exposure only as an `mcp:` literal, and
|
|
584
|
+
with exposure derived from the guards the resolved catalogue is the only place that can answer.
|
|
567
585
|
- A browser-hosted client needs `allowedOrigins` **and** the CORS answer the server sends back for those origins.
|
|
568
586
|
Every other MCP client sends no `Origin` at all, and the one that does is matched against the forwarded host so
|
|
569
587
|
a proxy does not turn each call into a 403 — which is only as trustworthy as an edge that *overwrites* that
|
|
@@ -588,9 +606,8 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
588
606
|
token signed wrong, like an opaque one, still degrades to an anonymous caller.
|
|
589
607
|
- **Resource URIs**: `akan://<model>/{id}`, `akan://<model>/light/{id}`, `akan://<model>/list` for the model's own
|
|
590
608
|
list, and `akan://<model>/list/<sliceKey>` for a slice's. The root list takes no third segment on purpose — any
|
|
591
|
-
token there is one a slice could also be named. **Those four are the whole set**, so
|
|
592
|
-
|
|
593
|
-
named in the boot log saying so.
|
|
609
|
+
token there is one a slice could also be named. **Those four are the whole set**, so only the generated reads are
|
|
610
|
+
addressable: a custom endpoint keeps its tool and gets no resource template.
|
|
594
611
|
- **The catalogue is one language**, `en` unless `language` says otherwise: it is built once at boot and cached by
|
|
595
612
|
clients, so there is no `Accept-Language` negotiation.
|
|
596
613
|
|
|
@@ -616,6 +633,68 @@ frames down included, and is a no-op when nobody is streaming — so the same co
|
|
|
616
633
|
websocket, and in tests. `McpProgress.streaming` says whether anyone is reading, for a report whose message
|
|
617
634
|
costs something to assemble.
|
|
618
635
|
|
|
636
|
+
## In-Page Agent
|
|
637
|
+
|
|
638
|
+
Every akan app can host a component-level agent that reads the rendered screen and drives it. Tools, state, and
|
|
639
|
+
context are **derived from the rendered screen, not from the bundle**: a store joins the surface only while a
|
|
640
|
+
mounted component reads one of its keys (`st.use` / `st.sel` / `st.ref` all count), and only that store's
|
|
641
|
+
catalogued actions and state are published. `Load` scopes, the route, and the live keys complete the context, so
|
|
642
|
+
most screens publish a full surface with zero agent code. The React core is the `use-agentic` package; apps and
|
|
643
|
+
libs never import it directly (`no-import-external-library`) — everything reaches them through `st.*` and
|
|
644
|
+
`akanjs/ui`.
|
|
645
|
+
|
|
646
|
+
- **Mount `<Agent.Chat />` once in a layout.** That is the floating chat, the approval card, and the client-side
|
|
647
|
+
loop. The default runner drives `runAgentTurn`, which the **framework serves on every app** — no lib to mount,
|
|
648
|
+
`AKAN_AGENT=false` takes it off — and negotiates streaming via `accept`, so assistant text arrives as it is
|
|
649
|
+
generated with zero app code. The endpoint is a stateless relay and **never executes tools**: every tool runs in
|
|
650
|
+
the caller's own browser session, gated by guards and the approval card. Its guard is `AgentRelayAccess`, which
|
|
651
|
+
defaults to allow and **warns at boot while no policy is registered** — a product with accounts locks it in its
|
|
652
|
+
`option.ts`, `option.setAgentAccess((ctx) => !!ctx.get("account"))`, or anonymous visitors spend the LLM key.
|
|
653
|
+
`persist` keeps the transcript across reloads (sessionStorage; `{ storage: "local" }` to outlive the tab),
|
|
654
|
+
default off. Re-skin through the `AgentChat` slot in `_overrides.tsx`.
|
|
655
|
+
- **The LLM is configured in `option.ts`, never through the environment.** `option.setLlm({ apiKey, model, host })`
|
|
656
|
+
— or `setLlm((options) => …)` to read the key out of the app's own env object, which is where a secret belongs —
|
|
657
|
+
fills whichever adaptor holds `LlmAdaptorRole`, reaching it as the `llmOption` use. The settings are the role's
|
|
658
|
+
rather than one provider's, so they survive a swap. **DeepSeek is the built-in default** (`deepseek-chat` at
|
|
659
|
+
`https://api.deepseek.com`); with no `apiKey` the app still boots and the chat answers `llmUnavailable`. Swap
|
|
660
|
+
providers the way middleware is applied: `option.applyAdaptor(LlmAdaptorRole, ClaudeLlm)`, where the
|
|
661
|
+
implementation is an `adapt()` class in a `srvkit/` implementing `LlmAdaptor.chat(request, onDelta?)` — ignore
|
|
662
|
+
`onDelta` and the chat still answers whole.
|
|
663
|
+
- **`<Agent.Zone id="comments">` runs a second agent over one section, in parallel with the root.** Everything
|
|
664
|
+
mounted inside — `st.use` subscriptions, hook tools, Guides — belongs to that zone's own conversation *and*
|
|
665
|
+
stays visible to the root agent: **zones are views, never walls**, so wrapping a section costs the root nothing.
|
|
666
|
+
An `Agent.Chat` inside binds to the zone session automatically; a zone's `readScreen` reads only its own
|
|
667
|
+
`data-agent-zone` container; guides follow the layout cascade (ancestors and own, never a sibling's). Zone
|
|
668
|
+
membership is positional — there is no per-declaration zone key, so a lib component joins whatever zone the app
|
|
669
|
+
mounts it in.
|
|
670
|
+
- **Route guidance is `<Agent.Guide instructions="..." />`** rendered from a `_layout.tsx` or a page — the render
|
|
671
|
+
tree is the cascade: nested Guides concatenate outer-to-inner and navigating away withdraws them. It is a
|
|
672
|
+
component, not a pageConfig field. Module `*.abstract.md` files are developer docs and are never served to the
|
|
673
|
+
agent.
|
|
674
|
+
- **Exposure is the store author's to trim.** `static agent = false` on a store class keeps the whole module off
|
|
675
|
+
the surface (the framework's base store declares it — its keys are plumbing and `tryJwt` is a credential);
|
|
676
|
+
`static agent = { exclude: ["setMapBounds", "mapCamera"] }` withholds named actions and state keys that are not
|
|
677
|
+
real levers (state a component writes into but never reads back). `st.use.x({ agent: false })` subscribes
|
|
678
|
+
without counting toward liveness. Generated `set<Key>` conveniences are never published — declare a typed action
|
|
679
|
+
or `st.tool` when an agent should set one.
|
|
680
|
+
- **Hooks are the escape hatch, not the norm.** `st.useState(name, initial, meta)` publishes local state
|
|
681
|
+
(read-only unless `set:` names a type), `st.expose(name, value)` a derived value, and
|
|
682
|
+
`st.tool("x", { desc }).arg("id", ID).exec(fn)` a one-off action. `.exec()` is the only hook, so the chain
|
|
683
|
+
completes in one unconditional statement; its callable carries `data-akan-action`, so pass it to `onClick` by
|
|
684
|
+
reference like a store setter. `remove*`-named tools default to a confirm gate.
|
|
685
|
+
- **Model-facing text is English, always** — tool `desc`, `instructions`, Guide text. The `l()` rule covers
|
|
686
|
+
strings a *user* reads: Chat's own buttons go through `l("base.*")`, the model's text never does.
|
|
687
|
+
- A masked model never crosses the boundary: a value whose `hidden`/`secret` fields are populated is refused at
|
|
688
|
+
read unless a `mask:` model is named — the same rule and wording as `AgentBridge.read`.
|
|
689
|
+
- **`prompt()` endpoints double as the chat's slash commands.** There is no listing endpoint — the client reads
|
|
690
|
+
its own serialized signals — so a prompt's dictionary `.desc()` is what the menu shows, and its guards are
|
|
691
|
+
enforced by the prompt's own GET at call time.
|
|
692
|
+
- The framework publishes three built-ins on every store surface: `navigate` (internal paths only, the same
|
|
693
|
+
router `Link` rides), `readScreen` (the rendered DOM as compact text — headings, links, control values; the
|
|
694
|
+
chat's own UI is skipped via `data-agent-ui`, and a password value is never read), and `readState(key)` (one
|
|
695
|
+
masked store key). Declaring a store action or hook tool under one of those names shadows the built-in, so
|
|
696
|
+
reuse them only to mean that.
|
|
697
|
+
|
|
619
698
|
## Scalar Modeling (`**/*.constant.ts`)
|
|
620
699
|
|
|
621
700
|
- Define Akan models in `.constant.ts` files with `via` from `akanjs/constant`.
|