apcore-js 0.1.0
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/.claude/settings.local.json +11 -0
- package/.gitmessage +60 -0
- package/.pre-commit-config.yaml +28 -0
- package/CHANGELOG.md +47 -0
- package/CLAUDE.md +68 -0
- package/README.md +131 -0
- package/apcore-logo.svg +79 -0
- package/package.json +37 -0
- package/planning/acl-system/overview.md +54 -0
- package/planning/acl-system/plan.md +92 -0
- package/planning/acl-system/state.json +76 -0
- package/planning/acl-system/tasks/acl-core.md +226 -0
- package/planning/acl-system/tasks/acl-rule.md +92 -0
- package/planning/acl-system/tasks/conditional-rules.md +259 -0
- package/planning/acl-system/tasks/pattern-matching.md +152 -0
- package/planning/acl-system/tasks/yaml-loading.md +271 -0
- package/planning/core-executor/overview.md +53 -0
- package/planning/core-executor/plan.md +88 -0
- package/planning/core-executor/state.json +76 -0
- package/planning/core-executor/tasks/async-support.md +106 -0
- package/planning/core-executor/tasks/execution-pipeline.md +113 -0
- package/planning/core-executor/tasks/redaction.md +85 -0
- package/planning/core-executor/tasks/safety-checks.md +65 -0
- package/planning/core-executor/tasks/setup.md +75 -0
- package/planning/decorator-bindings/overview.md +62 -0
- package/planning/decorator-bindings/plan.md +104 -0
- package/planning/decorator-bindings/state.json +87 -0
- package/planning/decorator-bindings/tasks/binding-directory.md +79 -0
- package/planning/decorator-bindings/tasks/binding-loader.md +148 -0
- package/planning/decorator-bindings/tasks/explicit-schemas.md +85 -0
- package/planning/decorator-bindings/tasks/function-module.md +127 -0
- package/planning/decorator-bindings/tasks/module-factory.md +89 -0
- package/planning/decorator-bindings/tasks/schema-modes.md +142 -0
- package/planning/middleware-system/overview.md +48 -0
- package/planning/middleware-system/plan.md +102 -0
- package/planning/middleware-system/state.json +65 -0
- package/planning/middleware-system/tasks/adapters.md +170 -0
- package/planning/middleware-system/tasks/base.md +115 -0
- package/planning/middleware-system/tasks/logging-middleware.md +304 -0
- package/planning/middleware-system/tasks/manager.md +313 -0
- package/planning/observability/overview.md +53 -0
- package/planning/observability/plan.md +119 -0
- package/planning/observability/state.json +98 -0
- package/planning/observability/tasks/context-logger.md +201 -0
- package/planning/observability/tasks/exporters.md +121 -0
- package/planning/observability/tasks/metrics-collector.md +162 -0
- package/planning/observability/tasks/metrics-middleware.md +141 -0
- package/planning/observability/tasks/obs-logging-middleware.md +179 -0
- package/planning/observability/tasks/span-model.md +120 -0
- package/planning/observability/tasks/tracing-middleware.md +179 -0
- package/planning/overview.md +81 -0
- package/planning/registry-system/overview.md +57 -0
- package/planning/registry-system/plan.md +114 -0
- package/planning/registry-system/state.json +109 -0
- package/planning/registry-system/tasks/dependencies.md +157 -0
- package/planning/registry-system/tasks/entry-point.md +148 -0
- package/planning/registry-system/tasks/metadata.md +198 -0
- package/planning/registry-system/tasks/registry-core.md +323 -0
- package/planning/registry-system/tasks/scanner.md +172 -0
- package/planning/registry-system/tasks/schema-export.md +261 -0
- package/planning/registry-system/tasks/types.md +124 -0
- package/planning/registry-system/tasks/validation.md +177 -0
- package/planning/schema-system/overview.md +56 -0
- package/planning/schema-system/plan.md +121 -0
- package/planning/schema-system/state.json +98 -0
- package/planning/schema-system/tasks/exporter.md +153 -0
- package/planning/schema-system/tasks/loader.md +106 -0
- package/planning/schema-system/tasks/ref-resolver.md +133 -0
- package/planning/schema-system/tasks/strict-mode.md +140 -0
- package/planning/schema-system/tasks/typebox-generation.md +133 -0
- package/planning/schema-system/tasks/types-and-annotations.md +160 -0
- package/planning/schema-system/tasks/validator.md +149 -0
- package/src/acl.ts +188 -0
- package/src/bindings.ts +208 -0
- package/src/config.ts +24 -0
- package/src/context.ts +75 -0
- package/src/decorator.ts +110 -0
- package/src/errors.ts +369 -0
- package/src/executor.ts +348 -0
- package/src/index.ts +81 -0
- package/src/middleware/adapters.ts +54 -0
- package/src/middleware/base.ts +33 -0
- package/src/middleware/index.ts +6 -0
- package/src/middleware/logging.ts +103 -0
- package/src/middleware/manager.ts +105 -0
- package/src/module.ts +41 -0
- package/src/observability/context-logger.ts +201 -0
- package/src/observability/index.ts +4 -0
- package/src/observability/metrics.ts +212 -0
- package/src/observability/tracing.ts +187 -0
- package/src/registry/dependencies.ts +99 -0
- package/src/registry/entry-point.ts +64 -0
- package/src/registry/index.ts +8 -0
- package/src/registry/metadata.ts +111 -0
- package/src/registry/registry.ts +314 -0
- package/src/registry/scanner.ts +150 -0
- package/src/registry/schema-export.ts +177 -0
- package/src/registry/types.ts +32 -0
- package/src/registry/validation.ts +38 -0
- package/src/schema/annotations.ts +67 -0
- package/src/schema/exporter.ts +93 -0
- package/src/schema/index.ts +14 -0
- package/src/schema/loader.ts +270 -0
- package/src/schema/ref-resolver.ts +235 -0
- package/src/schema/strict.ts +128 -0
- package/src/schema/types.ts +73 -0
- package/src/schema/validator.ts +82 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/pattern.ts +30 -0
- package/tests/helpers.ts +30 -0
- package/tests/integration/test-acl-safety.test.ts +268 -0
- package/tests/integration/test-binding-executor.test.ts +194 -0
- package/tests/integration/test-e2e-flow.test.ts +117 -0
- package/tests/integration/test-error-propagation.test.ts +259 -0
- package/tests/integration/test-middleware-chain.test.ts +120 -0
- package/tests/integration/test-observability-integration.test.ts +438 -0
- package/tests/observability/test-context-logger.test.ts +123 -0
- package/tests/observability/test-metrics.test.ts +89 -0
- package/tests/observability/test-tracing.test.ts +131 -0
- package/tests/registry/test-dependencies.test.ts +70 -0
- package/tests/registry/test-entry-point.test.ts +133 -0
- package/tests/registry/test-metadata.test.ts +265 -0
- package/tests/registry/test-registry.test.ts +140 -0
- package/tests/registry/test-scanner.test.ts +257 -0
- package/tests/registry/test-schema-export.test.ts +224 -0
- package/tests/registry/test-validation.test.ts +75 -0
- package/tests/schema/test-loader.test.ts +97 -0
- package/tests/schema/test-ref-resolver.test.ts +105 -0
- package/tests/schema/test-strict.test.ts +139 -0
- package/tests/schema/test-validator.test.ts +64 -0
- package/tests/test-acl.test.ts +206 -0
- package/tests/test-bindings.test.ts +227 -0
- package/tests/test-config.test.ts +76 -0
- package/tests/test-context.test.ts +151 -0
- package/tests/test-decorator.test.ts +173 -0
- package/tests/test-errors.test.ts +204 -0
- package/tests/test-executor.test.ts +252 -0
- package/tests/test-middleware-manager.test.ts +185 -0
- package/tests/test-middleware.test.ts +86 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +18 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Task: matchPattern() Wildcard Matching (Algorithm A08)
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `matchPattern()` utility function that performs wildcard glob-style pattern matching against module IDs. The algorithm (designated A08) splits patterns on `*` delimiters and verifies each literal segment appears in order within the target string, supporting prefix, suffix, infix, and multi-wildcard patterns.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/utils/pattern.ts` -- Standalone utility function (~30 lines)
|
|
10
|
+
|
|
11
|
+
## Steps (TDD)
|
|
12
|
+
|
|
13
|
+
### 1. Write failing tests for all pattern matching cases
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// tests/utils/pattern.test.ts
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { matchPattern } from '../../src/utils/pattern.js';
|
|
19
|
+
|
|
20
|
+
describe('matchPattern (Algorithm A08)', () => {
|
|
21
|
+
describe('exact matching', () => {
|
|
22
|
+
it('should match identical strings', () => {
|
|
23
|
+
expect(matchPattern('moduleA', 'moduleA')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should reject non-matching strings', () => {
|
|
27
|
+
expect(matchPattern('moduleA', 'moduleB')).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('bare wildcard', () => {
|
|
32
|
+
it('should match any string with bare *', () => {
|
|
33
|
+
expect(matchPattern('*', 'anything')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should match empty string with bare *', () => {
|
|
37
|
+
expect(matchPattern('*', '')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('prefix wildcard', () => {
|
|
42
|
+
it('should match suffix with *suffix pattern', () => {
|
|
43
|
+
expect(matchPattern('*.handler', 'auth.handler')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should reject non-matching suffix', () => {
|
|
47
|
+
expect(matchPattern('*.handler', 'auth.controller')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('suffix wildcard', () => {
|
|
52
|
+
it('should match prefix with prefix* pattern', () => {
|
|
53
|
+
expect(matchPattern('auth.*', 'auth.login')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should reject non-matching prefix', () => {
|
|
57
|
+
expect(matchPattern('auth.*', 'user.login')).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('infix wildcard', () => {
|
|
62
|
+
it('should match with pattern containing middle wildcard', () => {
|
|
63
|
+
expect(matchPattern('auth.*.handler', 'auth.login.handler')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should reject when middle segment is absent', () => {
|
|
67
|
+
expect(matchPattern('auth.*.handler', 'user.login.handler')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('multi-wildcard', () => {
|
|
72
|
+
it('should match with multiple wildcards', () => {
|
|
73
|
+
expect(matchPattern('a*b*c', 'aXXbYYc')).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle adjacent wildcards', () => {
|
|
77
|
+
expect(matchPattern('a**b', 'aXb')).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('edge cases', () => {
|
|
82
|
+
it('should handle pattern longer than moduleId', () => {
|
|
83
|
+
expect(matchPattern('very.long.pattern', 'short')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle empty pattern with non-empty moduleId', () => {
|
|
87
|
+
expect(matchPattern('', '')).toBe(true);
|
|
88
|
+
expect(matchPattern('', 'notempty')).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2. Implement matchPattern with segment-based algorithm
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// src/utils/pattern.ts
|
|
98
|
+
export function matchPattern(pattern: string, moduleId: string): boolean {
|
|
99
|
+
if (pattern === '*') return true;
|
|
100
|
+
if (!pattern.includes('*')) return pattern === moduleId;
|
|
101
|
+
|
|
102
|
+
const segments = pattern.split('*');
|
|
103
|
+
let pos = 0;
|
|
104
|
+
|
|
105
|
+
// Check prefix (first segment before first *)
|
|
106
|
+
if (!pattern.startsWith('*')) {
|
|
107
|
+
if (!moduleId.startsWith(segments[0])) return false;
|
|
108
|
+
pos = segments[0].length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check each intermediate segment appears in order
|
|
112
|
+
for (let i = 1; i < segments.length; i++) {
|
|
113
|
+
const segment = segments[i];
|
|
114
|
+
if (!segment) continue; // empty segment from adjacent ** or trailing *
|
|
115
|
+
const idx = moduleId.indexOf(segment, pos);
|
|
116
|
+
if (idx === -1) return false;
|
|
117
|
+
pos = idx + segment.length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check suffix (last segment after last *)
|
|
121
|
+
if (!pattern.endsWith('*')) {
|
|
122
|
+
if (!moduleId.endsWith(segments[segments.length - 1])) return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Run tests and verify
|
|
130
|
+
|
|
131
|
+
Run `vitest` to confirm all pattern matching tests pass. Run `tsc --noEmit` to verify type safety.
|
|
132
|
+
|
|
133
|
+
## Acceptance Criteria
|
|
134
|
+
|
|
135
|
+
- [x] `matchPattern()` is exported from `src/utils/pattern.ts`
|
|
136
|
+
- [x] Bare `*` matches any string (including empty)
|
|
137
|
+
- [x] No `*` in pattern means exact string match
|
|
138
|
+
- [x] Prefix wildcard (`*.suffix`) matches strings ending with the suffix
|
|
139
|
+
- [x] Suffix wildcard (`prefix.*`) matches strings starting with the prefix
|
|
140
|
+
- [x] Infix wildcard (`prefix.*.suffix`) matches strings with the prefix and suffix surrounding any middle content
|
|
141
|
+
- [x] Multiple wildcards (`a*b*c`) verified by sequential segment search
|
|
142
|
+
- [x] Adjacent wildcards (`a**b`) handled correctly (empty segments are skipped)
|
|
143
|
+
- [x] Algorithm runs in O(n * m) worst case where n = moduleId length, m = number of segments
|
|
144
|
+
- [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
145
|
+
|
|
146
|
+
## Dependencies
|
|
147
|
+
|
|
148
|
+
- None (standalone utility with no imports)
|
|
149
|
+
|
|
150
|
+
## Estimated Time
|
|
151
|
+
|
|
152
|
+
2 hours
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Task: ACL.load() from YAML and reload() Support
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the static `ACL.load()` factory method that parses YAML configuration files into a validated `ACL` instance, and the `reload()` instance method that re-reads the original YAML file to hot-swap rules without reconstructing the ACL object.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/acl.ts` -- `ACL.load()` static method and `reload()` instance method
|
|
10
|
+
- `src/errors.ts` -- `ACLRuleError` (invalid YAML/structure), `ConfigNotFoundError` (missing file)
|
|
11
|
+
|
|
12
|
+
## Steps (TDD)
|
|
13
|
+
|
|
14
|
+
### 1. Write failing tests for ACL.load() valid YAML
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// tests/acl.test.ts
|
|
18
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
19
|
+
import { writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
20
|
+
import { ACL } from '../src/acl.js';
|
|
21
|
+
import { ACLRuleError, ConfigNotFoundError } from '../src/errors.js';
|
|
22
|
+
|
|
23
|
+
describe('ACL.load()', () => {
|
|
24
|
+
const tmpDir = '/tmp/acl-test';
|
|
25
|
+
const yamlPath = `${tmpDir}/acl.yaml`;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
try { unlinkSync(yamlPath); } catch {}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should load valid YAML with rules and default_effect', () => {
|
|
36
|
+
writeFileSync(yamlPath, `
|
|
37
|
+
default_effect: allow
|
|
38
|
+
rules:
|
|
39
|
+
- callers: ["moduleA"]
|
|
40
|
+
targets: ["moduleB"]
|
|
41
|
+
effect: allow
|
|
42
|
+
description: "Allow A to B"
|
|
43
|
+
`);
|
|
44
|
+
const acl = ACL.load(yamlPath);
|
|
45
|
+
expect(acl.check('moduleA', 'moduleB')).toBe(true);
|
|
46
|
+
expect(acl.check('moduleX', 'moduleY')).toBe(true); // default allow
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should default to deny when default_effect is not specified', () => {
|
|
50
|
+
writeFileSync(yamlPath, `
|
|
51
|
+
rules:
|
|
52
|
+
- callers: ["moduleA"]
|
|
53
|
+
targets: ["moduleB"]
|
|
54
|
+
effect: allow
|
|
55
|
+
description: "test"
|
|
56
|
+
`);
|
|
57
|
+
const acl = ACL.load(yamlPath);
|
|
58
|
+
expect(acl.check('moduleX', 'moduleY')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should load rules with conditions', () => {
|
|
62
|
+
writeFileSync(yamlPath, `
|
|
63
|
+
rules:
|
|
64
|
+
- callers: ["*"]
|
|
65
|
+
targets: ["admin.*"]
|
|
66
|
+
effect: allow
|
|
67
|
+
description: "Admin access with role check"
|
|
68
|
+
conditions:
|
|
69
|
+
roles: ["admin"]
|
|
70
|
+
`);
|
|
71
|
+
const acl = ACL.load(yamlPath);
|
|
72
|
+
// Without context, conditions fail
|
|
73
|
+
expect(acl.check('moduleA', 'admin.panel')).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. Write failing tests for ACL.load() error cases
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
describe('ACL.load() error handling', () => {
|
|
82
|
+
it('should throw ConfigNotFoundError for missing file', () => {
|
|
83
|
+
expect(() => ACL.load('/nonexistent/acl.yaml')).toThrow(ConfigNotFoundError);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should throw ACLRuleError for invalid YAML', () => {
|
|
87
|
+
writeFileSync(yamlPath, '{ invalid yaml :::');
|
|
88
|
+
expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should throw ACLRuleError when rules key is missing', () => {
|
|
92
|
+
writeFileSync(yamlPath, 'default_effect: allow\n');
|
|
93
|
+
expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should throw ACLRuleError for invalid effect value', () => {
|
|
97
|
+
writeFileSync(yamlPath, `
|
|
98
|
+
rules:
|
|
99
|
+
- callers: ["*"]
|
|
100
|
+
targets: ["*"]
|
|
101
|
+
effect: maybe
|
|
102
|
+
description: "bad effect"
|
|
103
|
+
`);
|
|
104
|
+
expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should throw ACLRuleError when rule is not a mapping', () => {
|
|
108
|
+
writeFileSync(yamlPath, `
|
|
109
|
+
rules:
|
|
110
|
+
- "not a mapping"
|
|
111
|
+
`);
|
|
112
|
+
expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should throw ACLRuleError when rule is missing required keys', () => {
|
|
116
|
+
writeFileSync(yamlPath, `
|
|
117
|
+
rules:
|
|
118
|
+
- callers: ["*"]
|
|
119
|
+
description: "missing targets and effect"
|
|
120
|
+
`);
|
|
121
|
+
expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3. Implement ACL.load() static method
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
static load(yamlPath: string): ACL {
|
|
130
|
+
if (!existsSync(yamlPath)) {
|
|
131
|
+
throw new ConfigNotFoundError(yamlPath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let data: unknown;
|
|
135
|
+
try {
|
|
136
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
137
|
+
data = yaml.load(content);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e instanceof ConfigNotFoundError) throw e;
|
|
140
|
+
throw new ACLRuleError(`Invalid YAML in ${yamlPath}: ${e}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Validate top-level structure
|
|
144
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
145
|
+
throw new ACLRuleError(`ACL config must be a mapping, got ${typeof data}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const dataObj = data as Record<string, unknown>;
|
|
149
|
+
if (!('rules' in dataObj)) {
|
|
150
|
+
throw new ACLRuleError("ACL config missing required 'rules' key");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rawRules = dataObj['rules'];
|
|
154
|
+
if (!Array.isArray(rawRules)) {
|
|
155
|
+
throw new ACLRuleError(`'rules' must be a list, got ${typeof rawRules}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const defaultEffect = (dataObj['default_effect'] as string) ?? 'deny';
|
|
159
|
+
const rules: ACLRule[] = [];
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < rawRules.length; i++) {
|
|
162
|
+
const rawRule = rawRules[i];
|
|
163
|
+
// Validate each rule is a mapping with required keys
|
|
164
|
+
if (typeof rawRule !== 'object' || rawRule === null || Array.isArray(rawRule)) {
|
|
165
|
+
throw new ACLRuleError(`Rule ${i} must be a mapping, got ${typeof rawRule}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ruleObj = rawRule as Record<string, unknown>;
|
|
169
|
+
for (const key of ['callers', 'targets', 'effect']) {
|
|
170
|
+
if (!(key in ruleObj)) {
|
|
171
|
+
throw new ACLRuleError(`Rule ${i} missing required key '${key}'`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const effect = ruleObj['effect'] as string;
|
|
176
|
+
if (effect !== 'allow' && effect !== 'deny') {
|
|
177
|
+
throw new ACLRuleError(`Rule ${i} has invalid effect '${effect}', must be 'allow' or 'deny'`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
rules.push({
|
|
181
|
+
callers: ruleObj['callers'] as string[],
|
|
182
|
+
targets: ruleObj['targets'] as string[],
|
|
183
|
+
effect,
|
|
184
|
+
description: (ruleObj['description'] as string) ?? '',
|
|
185
|
+
conditions: (ruleObj['conditions'] as Record<string, unknown>) ?? null,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const acl = new ACL(rules, defaultEffect);
|
|
190
|
+
acl._yamlPath = yamlPath;
|
|
191
|
+
return acl;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 4. Write failing tests for reload()
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
describe('ACL.reload()', () => {
|
|
199
|
+
it('should reload rules from the original YAML path', () => {
|
|
200
|
+
writeFileSync(yamlPath, `
|
|
201
|
+
rules:
|
|
202
|
+
- callers: ["moduleA"]
|
|
203
|
+
targets: ["moduleB"]
|
|
204
|
+
effect: deny
|
|
205
|
+
description: "initial deny"
|
|
206
|
+
`);
|
|
207
|
+
const acl = ACL.load(yamlPath);
|
|
208
|
+
expect(acl.check('moduleA', 'moduleB')).toBe(false);
|
|
209
|
+
|
|
210
|
+
// Update YAML
|
|
211
|
+
writeFileSync(yamlPath, `
|
|
212
|
+
rules:
|
|
213
|
+
- callers: ["moduleA"]
|
|
214
|
+
targets: ["moduleB"]
|
|
215
|
+
effect: allow
|
|
216
|
+
description: "updated allow"
|
|
217
|
+
`);
|
|
218
|
+
acl.reload();
|
|
219
|
+
expect(acl.check('moduleA', 'moduleB')).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should throw ACLRuleError when not loaded from YAML', () => {
|
|
223
|
+
const acl = new ACL([]);
|
|
224
|
+
expect(() => acl.reload()).toThrow(ACLRuleError);
|
|
225
|
+
expect(() => acl.reload()).toThrow('Cannot reload: ACL was not loaded from a YAML file');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 5. Implement reload() method
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
reload(): void {
|
|
234
|
+
if (this._yamlPath === null) {
|
|
235
|
+
throw new ACLRuleError('Cannot reload: ACL was not loaded from a YAML file');
|
|
236
|
+
}
|
|
237
|
+
const reloaded = ACL.load(this._yamlPath);
|
|
238
|
+
this._rules = reloaded._rules;
|
|
239
|
+
this._defaultEffect = reloaded._defaultEffect;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### 6. Run full test suite and type-check
|
|
244
|
+
|
|
245
|
+
Run `tsc --noEmit` and `vitest` to confirm everything passes.
|
|
246
|
+
|
|
247
|
+
## Acceptance Criteria
|
|
248
|
+
|
|
249
|
+
- [x] `ACL.load()` reads YAML via `readFileSync` and parses with `js-yaml`
|
|
250
|
+
- [x] Throws `ConfigNotFoundError` when the YAML file does not exist
|
|
251
|
+
- [x] Throws `ACLRuleError` for invalid YAML syntax
|
|
252
|
+
- [x] Throws `ACLRuleError` when top-level structure is not a mapping
|
|
253
|
+
- [x] Throws `ACLRuleError` when `rules` key is missing
|
|
254
|
+
- [x] Throws `ACLRuleError` when `rules` is not an array
|
|
255
|
+
- [x] Validates each rule has `callers`, `targets`, and `effect` keys
|
|
256
|
+
- [x] Validates `effect` is either `'allow'` or `'deny'`
|
|
257
|
+
- [x] Validates `callers` and `targets` are arrays
|
|
258
|
+
- [x] Defaults `description` to empty string and `conditions` to `null`
|
|
259
|
+
- [x] Defaults `default_effect` to `'deny'` when not specified in YAML
|
|
260
|
+
- [x] Stores `_yamlPath` on the ACL instance for `reload()` support
|
|
261
|
+
- [x] `reload()` re-reads the YAML and replaces `_rules` and `_defaultEffect` in-place
|
|
262
|
+
- [x] `reload()` throws `ACLRuleError` when the ACL was not loaded from YAML
|
|
263
|
+
- [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
264
|
+
|
|
265
|
+
## Dependencies
|
|
266
|
+
|
|
267
|
+
- **acl-core** -- ACL class constructor and rule storage
|
|
268
|
+
|
|
269
|
+
## Estimated Time
|
|
270
|
+
|
|
271
|
+
2 hours
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Feature: Core Executor
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Core Execution Engine is the central orchestration component of apcore. It processes module calls through a structured 10-step pipeline: context creation, safety checks (call depth, circular detection, frequency throttling), module lookup from the registry, ACL enforcement, input validation with sensitive field redaction, middleware before chain, module execution with timeout enforcement, output validation, middleware after chain, and result return. The TypeScript implementation uses a single async `call()` method with `Promise.race` for timeout enforcement.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
### Included
|
|
10
|
+
|
|
11
|
+
- `Context` class and `Identity` interface for call metadata propagation and caller identity
|
|
12
|
+
- `Config` accessor with dot-path key support for executor settings
|
|
13
|
+
- `Executor` class implementing the full 10-step async pipeline
|
|
14
|
+
- Safety checks: call depth limits, circular call detection (cycles of length >= 2), frequency throttling
|
|
15
|
+
- Timeout enforcement via `Promise.race` with `setTimeout`
|
|
16
|
+
- `redactSensitive` utility for masking `x-sensitive` fields and `_secret_`-prefixed keys
|
|
17
|
+
- Structured error hierarchy (`ModuleError` base with specialized subclasses for every failure mode)
|
|
18
|
+
- Standalone `validate()` method for pre-flight schema checks without execution
|
|
19
|
+
- Middleware management via `MiddlewareManager` with before/after/onError chains
|
|
20
|
+
|
|
21
|
+
### Excluded
|
|
22
|
+
|
|
23
|
+
- Registry implementation (consumed as a dependency)
|
|
24
|
+
- Schema system internals (consumed via TypeBox validation)
|
|
25
|
+
- ACL rule definition and management (consumed via `ACL.check()` interface)
|
|
26
|
+
- Middleware implementation details (managed by `MiddlewareManager`)
|
|
27
|
+
|
|
28
|
+
## Technology Stack
|
|
29
|
+
|
|
30
|
+
- **TypeScript 5.5+** with strict mode
|
|
31
|
+
- **@sinclair/typebox >= 0.34.0** for input/output schema validation
|
|
32
|
+
- **Node.js >= 18.0.0** with ES Module support
|
|
33
|
+
- **vitest** for unit and integration testing
|
|
34
|
+
|
|
35
|
+
## Task Execution Order
|
|
36
|
+
|
|
37
|
+
| # | Task File | Description | Status |
|
|
38
|
+
|---|-----------|-------------|--------|
|
|
39
|
+
| 1 | [setup](./tasks/setup.md) | Context, Identity, and Config classes | completed |
|
|
40
|
+
| 2 | [safety-checks](./tasks/safety-checks.md) | Call depth limits, circular detection, frequency throttling | completed |
|
|
41
|
+
| 3 | [execution-pipeline](./tasks/execution-pipeline.md) | 10-step async execution pipeline with middleware and timeout | completed |
|
|
42
|
+
| 4 | [async-support](./tasks/async-support.md) | Unified async execution (single async call()) | completed |
|
|
43
|
+
| 5 | [redaction](./tasks/redaction.md) | Sensitive field redaction utility (`redactSensitive`) | completed |
|
|
44
|
+
|
|
45
|
+
## Progress
|
|
46
|
+
|
|
47
|
+
| Total | Completed | In Progress | Pending |
|
|
48
|
+
|-------|-----------|-------------|---------|
|
|
49
|
+
| 5 | 5 | 0 | 0 |
|
|
50
|
+
|
|
51
|
+
## Reference Documents
|
|
52
|
+
|
|
53
|
+
- [Core Executor Feature Specification](../../features/core-executor.md)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Implementation Plan: Core Executor
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the central 10-step execution pipeline that orchestrates all module calls in apcore, supporting context propagation, safety constraints, access control, schema validation with sensitive field redaction, middleware chains, and timeout-enforced module execution through a unified async code path.
|
|
6
|
+
|
|
7
|
+
## Architecture Design
|
|
8
|
+
|
|
9
|
+
### Component Structure
|
|
10
|
+
|
|
11
|
+
- **Executor** (`executor.ts`, ~300 lines) -- Main engine class implementing the 10-step pipeline. Accepts options object with `registry`, optional `middlewares` array, optional `acl`, and optional `config` at construction. Manages middleware registration via `MiddlewareManager`, configurable timeouts (default 30s, global 60s), max call depth (32), and max module repeat (3). Exposes `call()` (async) and `validate()` (pre-flight) entry points.
|
|
12
|
+
|
|
13
|
+
- **Context** (`context.ts`, ~60 lines) -- Class carrying per-call metadata: `traceId` (UUID v4 via `crypto.randomUUID()`), `callerId`, `callChain` (array of module IDs visited), `executor` reference, `identity`, `redactedInputs`, and a shared `data` record. Factory methods `create()` for root contexts and `child()` for nested calls. The `data` record is intentionally shared (not copied) between parent and child contexts to support middleware span/timing stacks.
|
|
14
|
+
|
|
15
|
+
- **Identity** (`context.ts`) -- Frozen interface representing the caller: `id`, `type` (default "user"), `roles` array, and `attrs` record. Created via `createIdentity()` factory with `Object.freeze()`.
|
|
16
|
+
|
|
17
|
+
- **Config** (`config.ts`, ~20 lines) -- Configuration accessor with dot-path key support (e.g., `executor.default_timeout`). Wraps a nested record and navigates through path segments.
|
|
18
|
+
|
|
19
|
+
- **Error Hierarchy** (`errors.ts`, ~280 lines) -- `ModuleError` base class with `timestamp`, `code`, `message`, `details` record, and `cause`. Specialized subclasses: `CallDepthExceededError`, `CircularCallError`, `CallFrequencyExceededError`, `ModuleNotFoundError`, `ACLDeniedError`, `SchemaValidationError`, `ModuleTimeoutError`, `InvalidInputError`.
|
|
20
|
+
|
|
21
|
+
### Data Flow
|
|
22
|
+
|
|
23
|
+
The 10-step pipeline processes every module call in this order:
|
|
24
|
+
|
|
25
|
+
1. **Context Creation** -- Build or derive `Context` via `create()` / `child()`
|
|
26
|
+
2. **Safety Checks** -- Call depth limit, circular detection (cycles >= 2), frequency throttling
|
|
27
|
+
3. **Module Lookup** -- `Registry.get()` with `ModuleNotFoundError` on miss
|
|
28
|
+
4. **ACL Enforcement** -- `ACL.check()` with `ACLDeniedError` on denial
|
|
29
|
+
5. **Input Validation** -- TypeBox `Value.Check()` + `redactSensitive()` for context logging
|
|
30
|
+
6. **Middleware Before** -- `MiddlewareManager.executeBefore()` with `MiddlewareChainError` handling
|
|
31
|
+
7. **Module Execution** -- Timeout via `Promise.race` with `setTimeout`
|
|
32
|
+
8. **Output Validation** -- TypeBox `Value.Check()` on return value
|
|
33
|
+
9. **Middleware After** -- `MiddlewareManager.executeAfter()` in reverse order
|
|
34
|
+
10. **Result Return** -- Return output record or propagate error with `onError` recovery
|
|
35
|
+
|
|
36
|
+
### Technical Choices and Rationale
|
|
37
|
+
|
|
38
|
+
- **`Promise.race` for timeout**: Simple and idiomatic for Node.js async timeout enforcement. The timeout promise rejects with `ModuleTimeoutError` if the execution exceeds the limit.
|
|
39
|
+
- **Unified async `call()`**: TypeScript's async-first model eliminates the need for separate sync/async code paths. All module executions go through `Promise.resolve()` which handles both sync and async return values transparently.
|
|
40
|
+
- **Shared `data` record in child contexts**: Enables middleware like tracing and metrics to maintain span stacks across nested module-to-module calls without additional plumbing.
|
|
41
|
+
- **`JSON.parse(JSON.stringify())` in `redactSensitive`**: Simple deep clone for JSON-compatible input data. Suitable since module inputs are expected to be JSON-serializable.
|
|
42
|
+
|
|
43
|
+
## Task Breakdown
|
|
44
|
+
|
|
45
|
+
```mermaid
|
|
46
|
+
graph TD
|
|
47
|
+
T1[setup] --> T2[safety-checks]
|
|
48
|
+
T1 --> T3[execution-pipeline]
|
|
49
|
+
T2 --> T3
|
|
50
|
+
T3 --> T4[async-support]
|
|
51
|
+
T1 --> T5[redaction]
|
|
52
|
+
T5 --> T3
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Task ID | Title | Estimated Time | Dependencies |
|
|
56
|
+
|---------|-------|---------------|--------------|
|
|
57
|
+
| setup | Context, Identity, and Config classes | 2h | none |
|
|
58
|
+
| safety-checks | Call depth, circular detection, frequency throttling | 3h | setup |
|
|
59
|
+
| execution-pipeline | 10-step async pipeline with middleware and timeout | 6h | setup, safety-checks, redaction |
|
|
60
|
+
| async-support | Unified async execution and Promise-based bridge | 4h | execution-pipeline |
|
|
61
|
+
| redaction | Sensitive field redaction utility | 2h | setup |
|
|
62
|
+
|
|
63
|
+
## Risks and Considerations
|
|
64
|
+
|
|
65
|
+
- **`setTimeout` timer leak**: The timeout timer is not cleaned up if execution completes before timeout. Harmless since the timer fires into a resolved promise, but wasteful.
|
|
66
|
+
- **Middleware error recovery**: If a middleware `before()` fails, previously executed middlewares must have their `onError()` called. The `MiddlewareChainError` wrapping captures which middlewares have been executed.
|
|
67
|
+
- **Circular detection strictness**: Only cycles of length >= 2 are detected (A calling B calling A), not self-recursion (A calling A), which is covered by frequency throttling.
|
|
68
|
+
|
|
69
|
+
## Acceptance Criteria
|
|
70
|
+
|
|
71
|
+
- [ ] All 10 pipeline steps are implemented and tested in isolation and end-to-end
|
|
72
|
+
- [ ] Timeout enforcement works via `Promise.race` with `setTimeout`
|
|
73
|
+
- [ ] Timeout of 0 disables enforcement with a logged warning
|
|
74
|
+
- [ ] Negative timeout raises `InvalidInputError`
|
|
75
|
+
- [ ] Safety checks correctly detect call depth violations, circular calls, and frequency throttling
|
|
76
|
+
- [ ] `redactSensitive` handles nested objects, arrays with `x-sensitive` items, and `_secret_`-prefixed keys
|
|
77
|
+
- [ ] Error recovery via `onError` returns recovery record or re-raises original exception
|
|
78
|
+
- [ ] `validate()` provides standalone pre-flight schema checks without execution
|
|
79
|
+
- [ ] All error types carry structured details with timestamps
|
|
80
|
+
- [ ] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
81
|
+
|
|
82
|
+
## References
|
|
83
|
+
|
|
84
|
+
- `src/executor.ts` -- Core execution engine
|
|
85
|
+
- `src/context.ts` -- Context and Identity classes
|
|
86
|
+
- `src/config.ts` -- Configuration accessor
|
|
87
|
+
- `src/errors.ts` -- Error hierarchy
|
|
88
|
+
- [Core Executor Feature Specification](../../features/core-executor.md)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"feature": "core-executor",
|
|
3
|
+
"created": "2026-02-16T00:00:00Z",
|
|
4
|
+
"updated": "2026-02-16T00:00:00Z",
|
|
5
|
+
"status": "completed",
|
|
6
|
+
"execution_order": [
|
|
7
|
+
"setup",
|
|
8
|
+
"safety-checks",
|
|
9
|
+
"execution-pipeline",
|
|
10
|
+
"async-support",
|
|
11
|
+
"redaction"
|
|
12
|
+
],
|
|
13
|
+
"progress": {
|
|
14
|
+
"total_tasks": 5,
|
|
15
|
+
"completed": 5,
|
|
16
|
+
"in_progress": 0,
|
|
17
|
+
"pending": 0
|
|
18
|
+
},
|
|
19
|
+
"tasks": [
|
|
20
|
+
{
|
|
21
|
+
"id": "setup",
|
|
22
|
+
"file": "tasks/setup.md",
|
|
23
|
+
"title": "Context, Identity, and Config Classes",
|
|
24
|
+
"status": "completed",
|
|
25
|
+
"started_at": "2026-02-16T08:00:00Z",
|
|
26
|
+
"completed_at": "2026-02-16T09:30:00Z",
|
|
27
|
+
"assignee": null,
|
|
28
|
+
"commits": []
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "safety-checks",
|
|
32
|
+
"file": "tasks/safety-checks.md",
|
|
33
|
+
"title": "Call Depth Limits, Circular Detection, Frequency Throttling",
|
|
34
|
+
"status": "completed",
|
|
35
|
+
"started_at": "2026-02-16T09:30:00Z",
|
|
36
|
+
"completed_at": "2026-02-16T11:00:00Z",
|
|
37
|
+
"assignee": null,
|
|
38
|
+
"commits": []
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "execution-pipeline",
|
|
42
|
+
"file": "tasks/execution-pipeline.md",
|
|
43
|
+
"title": "10-Step Async Execution Pipeline",
|
|
44
|
+
"status": "completed",
|
|
45
|
+
"started_at": "2026-02-16T11:00:00Z",
|
|
46
|
+
"completed_at": "2026-02-16T15:00:00Z",
|
|
47
|
+
"assignee": null,
|
|
48
|
+
"commits": []
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "async-support",
|
|
52
|
+
"file": "tasks/async-support.md",
|
|
53
|
+
"title": "Unified Async Execution Path",
|
|
54
|
+
"status": "completed",
|
|
55
|
+
"started_at": "2026-02-16T15:00:00Z",
|
|
56
|
+
"completed_at": "2026-02-16T18:00:00Z",
|
|
57
|
+
"assignee": null,
|
|
58
|
+
"commits": []
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "redaction",
|
|
62
|
+
"file": "tasks/redaction.md",
|
|
63
|
+
"title": "Sensitive Field Redaction Utility",
|
|
64
|
+
"status": "completed",
|
|
65
|
+
"started_at": "2026-02-16T18:00:00Z",
|
|
66
|
+
"completed_at": "2026-02-16T19:00:00Z",
|
|
67
|
+
"assignee": null,
|
|
68
|
+
"commits": []
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"metadata": {
|
|
72
|
+
"source_doc": "planning/features/core-executor.md",
|
|
73
|
+
"created_by": "code-forge",
|
|
74
|
+
"version": "1.0"
|
|
75
|
+
}
|
|
76
|
+
}
|