create-principles-disciple 1.102.0 → 1.104.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/console/dist/server/config/pd-config-store.d.ts +50 -1
- package/console/dist/server/config/pd-config-store.js +364 -1
- package/console/dist/server/models/ActivationsConsoleModel.d.ts +7 -4
- package/console/dist/server/models/ActivationsConsoleModel.js +7 -6
- package/console/dist/server/routes/config.js +92 -2
- package/console/dist/ui/api.d.ts +44 -3
- package/console/dist/ui/api.js +58 -2
- package/console/dist/ui/components/onboarding/DemoResultView.js +19 -1
- package/console/dist/ui/i18n/en.json +63 -2
- package/console/dist/ui/i18n/zh-CN.json +63 -2
- package/console/dist/ui/pages/activation/ActivationPage.js +11 -11
- package/console/dist/ui/pages/activation/ActivationValidators.js +14 -13
- package/console/dist/ui/pages/control-center/AgentCard.js +56 -7
- package/console/dist/ui/pages/control-center/ControlCenterPage.js +2 -1
- package/console/dist/ui/pages/control-center/RuntimeProfileManager.d.ts +31 -0
- package/console/dist/ui/pages/control-center/RuntimeProfileManager.js +234 -0
- package/console/dist/ui/pages/debt/DebtPage.js +2 -2
- package/console/dist/ui/utils/validators.d.ts +26 -3
- package/console/dist/ui/utils/validators.js +23 -6
- package/console/dist/web/assets/app.css +41 -0
- package/console/dist/web/assets/app.js +1918 -1160
- package/core/dist/runtime-v2/__tests__/resolve-runtime-config-from-pd-config.test.js +8 -10
- package/core/dist/runtime-v2/__tests__/resolve-runtime-config-from-pd-config.test.js.map +1 -1
- package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.d.ts +2 -0
- package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.js +81 -0
- package/core/dist/runtime-v2/adapter/__tests__/l2-resolve-model-compat.test.js.map +1 -0
- package/core/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js +25 -6
- package/core/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts +8 -0
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +4 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
- package/core/dist/runtime-v2/candidate-intake-service.d.ts.map +1 -1
- package/core/dist/runtime-v2/candidate-intake-service.js +92 -6
- package/core/dist/runtime-v2/candidate-intake-service.js.map +1 -1
- package/core/dist/runtime-v2/candidate-intake.d.ts +38 -0
- package/core/dist/runtime-v2/candidate-intake.d.ts.map +1 -1
- package/core/dist/runtime-v2/candidate-intake.js +48 -0
- package/core/dist/runtime-v2/candidate-intake.js.map +1 -1
- package/core/dist/runtime-v2/config/__tests__/pd-config-agent-binding.test.js +26 -1
- package/core/dist/runtime-v2/config/__tests__/pd-config-agent-binding.test.js.map +1 -1
- package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +9 -5
- package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -1
- package/core/dist/runtime-v2/config/__tests__/pd-config-validation-edge-cases.test.js +53 -33
- package/core/dist/runtime-v2/config/__tests__/pd-config-validation-edge-cases.test.js.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-agent-binding.d.ts +2 -0
- package/core/dist/runtime-v2/config/pd-config-agent-binding.d.ts.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-agent-binding.js +3 -0
- package/core/dist/runtime-v2/config/pd-config-agent-binding.js.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-defaults.d.ts +7 -3
- package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-defaults.js +27 -3
- package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-redaction.js +5 -2
- package/core/dist/runtime-v2/config/pd-config-redaction.js.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-types.d.ts +6 -0
- package/core/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-types.js.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-validate.js +11 -6
- package/core/dist/runtime-v2/config/pd-config-validate.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +2 -1
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +1 -0
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder-v2.test.js +7 -0
- package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder-v2.test.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.js +7 -6
- package/core/dist/runtime-v2/internalization/__tests__/diag-rootcause-runner.test.js.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +1 -1
- package/core/dist/runtime-v2/internalization/diag-distiller-runner.js +4 -4
- package/core/dist/runtime-v2/internalization/diag-distiller-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/diag-rootcause-runner.js +5 -5
- package/core/dist/runtime-v2/internalization/diag-rootcause-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/diag-router-runner.js +4 -4
- package/core/dist/runtime-v2/internalization/diag-router-runner.js.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts +2 -0
- package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js +2 -0
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +5 -1
- package/dist/installer.js.map +1 -1
- package/dist/mvp-config.d.ts.map +1 -1
- package/dist/mvp-config.js +22 -10
- package/dist/mvp-config.js.map +1 -1
- package/package.json +1 -1
- package/pd-cli/dist/commands/__tests__/runtime-probe-config.test.js +2 -2
- package/pd-cli/dist/commands/__tests__/runtime-probe-config.test.js.map +1 -1
- package/pd-cli/dist/commands/demo-story-a.d.ts.map +1 -1
- package/pd-cli/dist/commands/demo-story-a.js +4 -2
- package/pd-cli/dist/commands/demo-story-a.js.map +1 -1
- package/pd-cli/dist/commands/proven-channel-baseline.d.ts.map +1 -1
- package/pd-cli/dist/commands/proven-channel-baseline.js +4 -2
- package/pd-cli/dist/commands/proven-channel-baseline.js.map +1 -1
- package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-activation.js +8 -2
- package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
- package/pd-cli/dist/commands/runtime-canary.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-canary.js +4 -2
- package/pd-cli/dist/commands/runtime-canary.js.map +1 -1
- package/pd-cli/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-health-snapshot.js +4 -2
- package/pd-cli/dist/commands/runtime-health-snapshot.js.map +1 -1
- package/pd-cli/dist/commands/runtime-init.js +2 -2
- package/pd-cli/dist/commands/runtime-init.js.map +1 -1
- package/pd-cli/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-internalization-integrity.js +4 -2
- package/pd-cli/dist/commands/runtime-internalization-integrity.js.map +1 -1
- package/pd-cli/dist/services/config-doctor.d.ts.map +1 -1
- package/pd-cli/dist/services/config-doctor.js +9 -2
- package/pd-cli/dist/services/config-doctor.js.map +1 -1
- package/pd-cli/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
- package/pd-cli/dist/services/runtime-adapter-resolver.js +1 -0
- package/pd-cli/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/plugin/dist/commands/thinking-os.js +25 -7
- package/plugin/dist/commands/workflow-debug.d.ts +1 -3
- package/plugin/dist/commands/workflow-debug.js +26 -5
- package/plugin/dist/core/rule-host.d.ts +5 -2
- package/plugin/dist/core/rule-host.js +15 -2
- package/plugin/dist/hooks/after-tool-call-helpers.js +20 -3
- package/plugin/dist/hooks/gate.js +1 -1
- package/plugin/dist/i18n/commands.js +24 -0
- package/plugin/dist/index.js +88 -36
- package/plugin/dist/service/evolution-dedup.d.ts +1 -1
- package/plugin/dist/service/evolution-worker.d.ts +7 -31
- package/plugin/dist/service/evolution-worker.js +17 -36
- package/plugin/dist/service/queue-io.js +14 -2
- package/plugin/dist/service/queue-migration.d.ts +27 -26
- package/plugin/dist/service/queue-migration.js +57 -0
- package/plugin/docs/COMMAND_REFERENCE.md +0 -1
- package/plugin/docs/COMMAND_REFERENCE_EN.md +0 -2
- package/plugin/scripts/sync-plugin.mjs +12 -0
- package/plugin/templates/langs/en/core/AGENTS.md +2 -2
- package/plugin/templates/langs/en/core/TOOLS.md +1 -1
- package/plugin/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
- package/plugin/templates/langs/zh/core/AGENTS.md +2 -2
- package/plugin/templates/langs/zh/core/TOOLS.md +1 -1
- package/plugin/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
- package/templates/langs/en/core/AGENTS.md +2 -2
- package/templates/langs/en/core/BOOTSTRAP.md +4 -17
- package/templates/langs/en/core/TOOLS.md +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +8 -26
- package/templates/langs/zh/core/AGENTS.md +2 -2
- package/templates/langs/zh/core/BOOTSTRAP.md +4 -17
- package/templates/langs/zh/core/TOOLS.md +1 -1
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +8 -26
- package/plugin/dist/constants/diagnostician.d.ts +0 -12
- package/plugin/dist/constants/diagnostician.js +0 -65
- package/plugin/dist/service/evolution-queue-migration.d.ts +0 -128
- package/plugin/dist/service/evolution-queue-migration.js +0 -53
- package/plugin/templates/langs/en/skills/pd-diagnostician/SKILL.md +0 -466
- package/plugin/templates/langs/zh/skills/pd-diagnostician/SKILL.md +0 -391
- package/templates/langs/en/skills/pd-daily/SKILL.md +0 -199
- package/templates/langs/en/skills/pd-grooming/SKILL.md +0 -46
- package/templates/langs/zh/skills/pd-daily/SKILL.md +0 -283
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +0 -287
- package/templates/langs/zh/skills/pd-grooming/SKILL.md +0 -46
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - ERR-013: Object.hasOwn() for untrusted keys
|
|
14
14
|
* - ERR-045: ANY-segment redaction for sensitive keys
|
|
15
15
|
*/
|
|
16
|
-
import type { EffectivePdConfig, RedactedPdConfigSummary, InternalAgentName, FeatureFlagsResult } from '@principles/core/runtime-v2';
|
|
16
|
+
import type { EffectivePdConfig, RedactedPdConfigSummary, InternalAgentName, RuntimeProfile, FeatureFlagsResult } from '@principles/core/runtime-v2';
|
|
17
17
|
export interface ConfigLoadResultOk {
|
|
18
18
|
ok: true;
|
|
19
19
|
effective: EffectivePdConfig;
|
|
@@ -171,4 +171,53 @@ export type FeatureFlagUpdateResult = FeatureFlagUpdateResultOk | FeatureFlagUpd
|
|
|
171
171
|
* - ERR-015/018/019: load-validate-merge-write-reload, no stale cache
|
|
172
172
|
*/
|
|
173
173
|
export declare function updateFeatureFlag(workspaceDir: string, featureName: string, enabled: boolean): FeatureFlagUpdateResult;
|
|
174
|
+
export interface RuntimeProfileCrudResultOk {
|
|
175
|
+
ok: true;
|
|
176
|
+
profileId: string;
|
|
177
|
+
profile: RuntimeProfile;
|
|
178
|
+
}
|
|
179
|
+
export interface RuntimeProfileCrudResultErr {
|
|
180
|
+
ok: false;
|
|
181
|
+
statusCode: number;
|
|
182
|
+
error: string;
|
|
183
|
+
message: string;
|
|
184
|
+
nextAction?: string;
|
|
185
|
+
}
|
|
186
|
+
export type RuntimeProfileCrudResult = RuntimeProfileCrudResultOk | RuntimeProfileCrudResultErr;
|
|
187
|
+
/**
|
|
188
|
+
* Create a new runtime profile in .pd/config.yaml.
|
|
189
|
+
*
|
|
190
|
+
* - Rejects duplicate profile IDs (400 bad_request)
|
|
191
|
+
* - Validates profile structure (type + required fields via validatePdConfig)
|
|
192
|
+
* - Persists atomically, preserving unknown config sections
|
|
193
|
+
*
|
|
194
|
+
* ERR entries:
|
|
195
|
+
* - ERR-001/ERR-005: No `as` bypasses; payload validated as unknown
|
|
196
|
+
* - ERR-002: Graceful degradation includes reason + nextAction
|
|
197
|
+
* - ERR-009/ERR-010: Required fields fail loud
|
|
198
|
+
* - ERR-013: Object.hasOwn() for untrusted keys
|
|
199
|
+
*/
|
|
200
|
+
export declare function createRuntimeProfile(workspaceDir: string, profileId: string, payload: unknown): RuntimeProfileCrudResult;
|
|
201
|
+
/**
|
|
202
|
+
* Update an existing runtime profile in .pd/config.yaml.
|
|
203
|
+
*
|
|
204
|
+
* - Rejects non-existent profile (404 not_found)
|
|
205
|
+
* - Rejects type changes (400 bad_request — type change = delete + create)
|
|
206
|
+
* - Merges patch into existing profile, only known fields are persisted
|
|
207
|
+
* - Persists atomically, preserving unknown config sections
|
|
208
|
+
*
|
|
209
|
+
* ERR entries: same as createRuntimeProfile.
|
|
210
|
+
*/
|
|
211
|
+
export declare function updateRuntimeProfile(workspaceDir: string, profileId: string, patch: unknown): RuntimeProfileCrudResult;
|
|
212
|
+
/**
|
|
213
|
+
* Delete a runtime profile from .pd/config.yaml.
|
|
214
|
+
*
|
|
215
|
+
* - Rejects non-existent profile (404 not_found)
|
|
216
|
+
* - Rejects if profile is the defaultRuntime (400 bad_request)
|
|
217
|
+
* - Rejects if any agent binding references the profile (400 bad_request, lists agents)
|
|
218
|
+
* - Persists atomically, preserving unknown config sections
|
|
219
|
+
*
|
|
220
|
+
* ERR entries: same as createRuntimeProfile.
|
|
221
|
+
*/
|
|
222
|
+
export declare function deleteRuntimeProfile(workspaceDir: string, profileId: string): RuntimeProfileCrudResult;
|
|
174
223
|
export declare function checkReadiness(workspaceDir: string, agentName: string, getEnvVar?: (name: string) => string | undefined): ReadinessResult | ReadinessError;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import yaml from 'js-yaml';
|
|
19
|
-
import { validatePdConfig, computeEffectivePdConfig, computeFeatureFlagsFromConfig, redactPdConfig, checkAgentRuntimeReadiness, resolveAgentRuntimeBinding, INTERNAL_AGENT_NAMES, } from '@principles/core/runtime-v2';
|
|
19
|
+
import { validatePdConfig, computeEffectivePdConfig, computeFeatureFlagsFromConfig, redactPdConfig, checkAgentRuntimeReadiness, resolveAgentRuntimeBinding, INTERNAL_AGENT_NAMES, VALID_PROFILE_TYPES, } from '@principles/core/runtime-v2';
|
|
20
20
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
21
21
|
const PD_CONFIG_DIR = '.pd';
|
|
22
22
|
const PD_CONFIG_FILENAME = 'config.yaml';
|
|
@@ -802,6 +802,369 @@ export function updateFeatureFlag(workspaceDir, featureName, enabled) {
|
|
|
802
802
|
enabled: confirmedEnabled,
|
|
803
803
|
};
|
|
804
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Validate the basic structure of a runtime profile payload (type field).
|
|
807
|
+
* Full field validation is delegated to validatePdConfig after merge.
|
|
808
|
+
* Uses Object.hasOwn() for untrusted key checks (ERR-013).
|
|
809
|
+
*/
|
|
810
|
+
function validateProfileStructure(payload) {
|
|
811
|
+
if (!isRecord(payload)) {
|
|
812
|
+
return { ok: false, error: 'bad_request', message: 'Profile must be a JSON object with a type field' };
|
|
813
|
+
}
|
|
814
|
+
const typeRaw = Object.hasOwn(payload, 'type') ? payload.type : undefined;
|
|
815
|
+
if (typeRaw === undefined) {
|
|
816
|
+
return { ok: false, error: 'bad_request', message: `Profile missing required field: type (must be one of: ${VALID_PROFILE_TYPES.join(', ')})` };
|
|
817
|
+
}
|
|
818
|
+
if (typeof typeRaw !== 'string' || !VALID_PROFILE_TYPES.includes(typeRaw)) {
|
|
819
|
+
return { ok: false, error: 'bad_request', message: `Profile type must be one of: ${VALID_PROFILE_TYPES.join(', ')}. Got: ${String(typeRaw)}` };
|
|
820
|
+
}
|
|
821
|
+
return { ok: true, type: typeRaw };
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Extract only known, correctly-typed fields from a profile payload.
|
|
825
|
+
* Prevents unknown fields from being persisted to .pd/config.yaml.
|
|
826
|
+
* Uses Object.hasOwn() for untrusted key checks (ERR-013).
|
|
827
|
+
*/
|
|
828
|
+
function extractProfileFields(payload, type) {
|
|
829
|
+
const result = { type };
|
|
830
|
+
if (Object.hasOwn(payload, 'provider') && typeof payload.provider === 'string') {
|
|
831
|
+
result.provider = payload.provider;
|
|
832
|
+
}
|
|
833
|
+
if (Object.hasOwn(payload, 'model') && typeof payload.model === 'string') {
|
|
834
|
+
result.model = payload.model;
|
|
835
|
+
}
|
|
836
|
+
if (type === 'openclaw') {
|
|
837
|
+
if (Object.hasOwn(payload, 'source') && typeof payload.source === 'string') {
|
|
838
|
+
result.source = payload.source;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
// pi-ai
|
|
843
|
+
if (Object.hasOwn(payload, 'apiKeyEnv') && typeof payload.apiKeyEnv === 'string') {
|
|
844
|
+
result.apiKeyEnv = payload.apiKeyEnv;
|
|
845
|
+
}
|
|
846
|
+
if (Object.hasOwn(payload, 'baseUrl') && typeof payload.baseUrl === 'string') {
|
|
847
|
+
result.baseUrl = payload.baseUrl;
|
|
848
|
+
}
|
|
849
|
+
if (Object.hasOwn(payload, 'timeoutMs') && typeof payload.timeoutMs === 'number') {
|
|
850
|
+
result.timeoutMs = payload.timeoutMs;
|
|
851
|
+
}
|
|
852
|
+
if (Object.hasOwn(payload, 'maxRetries') && typeof payload.maxRetries === 'number') {
|
|
853
|
+
result.maxRetries = payload.maxRetries;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Read raw config file preserving unknown root sections.
|
|
860
|
+
* Returns an empty record if the file does not exist.
|
|
861
|
+
* Treats parsed YAML as unknown (ERR-001).
|
|
862
|
+
*/
|
|
863
|
+
function readRawConfig(configPath) {
|
|
864
|
+
if (!fs.existsSync(configPath)) {
|
|
865
|
+
return {};
|
|
866
|
+
}
|
|
867
|
+
const rawContent = fs.readFileSync(configPath, 'utf8');
|
|
868
|
+
const rawParsed = yaml.load(rawContent, { schema: yaml.JSON_SCHEMA });
|
|
869
|
+
if (isRecord(rawParsed)) {
|
|
870
|
+
return { ...rawParsed };
|
|
871
|
+
}
|
|
872
|
+
return {};
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Create a new runtime profile in .pd/config.yaml.
|
|
876
|
+
*
|
|
877
|
+
* - Rejects duplicate profile IDs (400 bad_request)
|
|
878
|
+
* - Validates profile structure (type + required fields via validatePdConfig)
|
|
879
|
+
* - Persists atomically, preserving unknown config sections
|
|
880
|
+
*
|
|
881
|
+
* ERR entries:
|
|
882
|
+
* - ERR-001/ERR-005: No `as` bypasses; payload validated as unknown
|
|
883
|
+
* - ERR-002: Graceful degradation includes reason + nextAction
|
|
884
|
+
* - ERR-009/ERR-010: Required fields fail loud
|
|
885
|
+
* - ERR-013: Object.hasOwn() for untrusted keys
|
|
886
|
+
*/
|
|
887
|
+
export function createRuntimeProfile(workspaceDir, profileId, payload) {
|
|
888
|
+
// 1. Validate profileId
|
|
889
|
+
if (typeof profileId !== 'string' || profileId.length === 0) {
|
|
890
|
+
return { ok: false, statusCode: 400, error: 'bad_request', message: 'profileId must be a non-empty string' };
|
|
891
|
+
}
|
|
892
|
+
// 2. Validate profile structure (type field)
|
|
893
|
+
const structResult = validateProfileStructure(payload);
|
|
894
|
+
if (!structResult.ok) {
|
|
895
|
+
return { ok: false, statusCode: 400, error: structResult.error, message: structResult.message };
|
|
896
|
+
}
|
|
897
|
+
// 3. Load existing config — refuse to write on malformed (rc-9)
|
|
898
|
+
const loadResult = loadPdConfig(workspaceDir);
|
|
899
|
+
if (!loadResult.ok) {
|
|
900
|
+
return {
|
|
901
|
+
ok: false,
|
|
902
|
+
statusCode: 409,
|
|
903
|
+
error: 'conflict',
|
|
904
|
+
message: `Cannot create runtime profile: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${loadResult.errors.map(e => e.reason).join('; ')}`,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
const { effective } = loadResult;
|
|
908
|
+
// 4. Reject duplicate profile ID
|
|
909
|
+
if (Object.hasOwn(effective.config.runtimeProfiles, profileId)) {
|
|
910
|
+
return {
|
|
911
|
+
ok: false,
|
|
912
|
+
statusCode: 400,
|
|
913
|
+
error: 'bad_request',
|
|
914
|
+
message: `Runtime profile '${profileId}' already exists. Use PATCH to update it.`,
|
|
915
|
+
nextAction: `Use PATCH /api/v1/config/profiles/${encodeURIComponent(profileId)} to update the existing profile`,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
// 5. Extract only known fields from payload (prevents unknown field persistence)
|
|
919
|
+
const profileRecord = extractProfileFields(payload, structResult.type);
|
|
920
|
+
// 6. Read raw config to preserve unknown sections, add new profile
|
|
921
|
+
const configPath = getPdConfigPath(workspaceDir);
|
|
922
|
+
const rawConfig = readRawConfig(configPath);
|
|
923
|
+
const profilesMap = isRecord(rawConfig.runtimeProfiles)
|
|
924
|
+
? { ...rawConfig.runtimeProfiles }
|
|
925
|
+
: { ...effective.config.runtimeProfiles };
|
|
926
|
+
profilesMap[profileId] = profileRecord;
|
|
927
|
+
rawConfig.runtimeProfiles = profilesMap;
|
|
928
|
+
// 7. Validate the full updated config (catches missing required fields etc.)
|
|
929
|
+
const validation = validatePdConfig(rawConfig);
|
|
930
|
+
if (!validation.ok) {
|
|
931
|
+
return {
|
|
932
|
+
ok: false,
|
|
933
|
+
statusCode: 400,
|
|
934
|
+
error: 'bad_request',
|
|
935
|
+
message: `Profile validation failed: ${validation.errors.map(e => e.reason).join('; ')}`,
|
|
936
|
+
nextAction: 'Fix profile fields and retry',
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
// 8. Atomic write
|
|
940
|
+
try {
|
|
941
|
+
writeConfigAtomic(configPath, rawConfig);
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
945
|
+
return { ok: false, statusCode: 500, error: 'write_error', message: `Failed to write config: ${message}`, nextAction: 'Check disk space and file permissions for .pd/config.yaml' };
|
|
946
|
+
}
|
|
947
|
+
// 9. Return the validated profile from the config
|
|
948
|
+
// Defensive guard: Object.hasOwn() doesn't narrow TS type, so extract and verify.
|
|
949
|
+
const createdProfile = validation.value.runtimeProfiles[profileId];
|
|
950
|
+
if (!createdProfile) {
|
|
951
|
+
return { ok: false, statusCode: 500, error: 'internal_error',
|
|
952
|
+
message: `Profile '${profileId}' was written but could not be read back from config.`,
|
|
953
|
+
nextAction: 'This is a bug in PD config persistence. Please report it.' };
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
ok: true,
|
|
957
|
+
profileId,
|
|
958
|
+
profile: createdProfile,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Update an existing runtime profile in .pd/config.yaml.
|
|
963
|
+
*
|
|
964
|
+
* - Rejects non-existent profile (404 not_found)
|
|
965
|
+
* - Rejects type changes (400 bad_request — type change = delete + create)
|
|
966
|
+
* - Merges patch into existing profile, only known fields are persisted
|
|
967
|
+
* - Persists atomically, preserving unknown config sections
|
|
968
|
+
*
|
|
969
|
+
* ERR entries: same as createRuntimeProfile.
|
|
970
|
+
*/
|
|
971
|
+
export function updateRuntimeProfile(workspaceDir, profileId, patch) {
|
|
972
|
+
// 1. Validate profileId
|
|
973
|
+
if (typeof profileId !== 'string' || profileId.length === 0) {
|
|
974
|
+
return { ok: false, statusCode: 400, error: 'bad_request', message: 'profileId must be a non-empty string' };
|
|
975
|
+
}
|
|
976
|
+
// 2. Validate patch is a record
|
|
977
|
+
if (!isRecord(patch)) {
|
|
978
|
+
return { ok: false, statusCode: 400, error: 'bad_request', message: 'Patch payload must be a JSON object' };
|
|
979
|
+
}
|
|
980
|
+
// 3. Load existing config — refuse to write on malformed (rc-9)
|
|
981
|
+
const loadResult = loadPdConfig(workspaceDir);
|
|
982
|
+
if (!loadResult.ok) {
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
statusCode: 409,
|
|
986
|
+
error: 'conflict',
|
|
987
|
+
message: `Cannot update runtime profile: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${loadResult.errors.map(e => e.reason).join('; ')}`,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const { effective } = loadResult;
|
|
991
|
+
// 4. Reject if profile doesn't exist (404)
|
|
992
|
+
if (!Object.hasOwn(effective.config.runtimeProfiles, profileId)) {
|
|
993
|
+
return {
|
|
994
|
+
ok: false,
|
|
995
|
+
statusCode: 404,
|
|
996
|
+
error: 'not_found',
|
|
997
|
+
message: `Runtime profile '${profileId}' does not exist. Available profiles: ${Object.keys(effective.config.runtimeProfiles).join(', ')}`,
|
|
998
|
+
nextAction: 'Use POST /api/v1/config/profiles to create a new profile',
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
const existingProfile = effective.config.runtimeProfiles[profileId];
|
|
1002
|
+
// Defensive guard: Object.hasOwn() above doesn't narrow TS type.
|
|
1003
|
+
if (!existingProfile) {
|
|
1004
|
+
return { ok: false, statusCode: 500, error: 'internal_error',
|
|
1005
|
+
message: `Profile '${profileId}' existence check passed but value is undefined.`,
|
|
1006
|
+
nextAction: 'This is a bug in PD config resolution. Please report it.' };
|
|
1007
|
+
}
|
|
1008
|
+
// 5. Reject type change (type change = delete + create)
|
|
1009
|
+
if (Object.hasOwn(patch, 'type')) {
|
|
1010
|
+
const patchType = patch.type;
|
|
1011
|
+
if (typeof patchType !== 'string' || patchType !== existingProfile.type) {
|
|
1012
|
+
return {
|
|
1013
|
+
ok: false,
|
|
1014
|
+
statusCode: 400,
|
|
1015
|
+
error: 'bad_request',
|
|
1016
|
+
message: `Cannot change profile type (existing: '${existingProfile.type}', requested: '${String(patchType)}'). Type change equals delete + create. Delete this profile and create a new one instead.`,
|
|
1017
|
+
nextAction: `DELETE /api/v1/config/profiles/${encodeURIComponent(profileId)} then POST /api/v1/config/profiles with the new type`,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// 6. Merge patch into existing profile, then extract only known fields
|
|
1022
|
+
const mergedSource = { ...existingProfile, ...patch };
|
|
1023
|
+
const mergedRecord = extractProfileFields(mergedSource, existingProfile.type);
|
|
1024
|
+
// 7. Read raw config to preserve unknown sections, update the profile
|
|
1025
|
+
const configPath = getPdConfigPath(workspaceDir);
|
|
1026
|
+
const rawConfig = readRawConfig(configPath);
|
|
1027
|
+
const profilesMap = isRecord(rawConfig.runtimeProfiles)
|
|
1028
|
+
? { ...rawConfig.runtimeProfiles }
|
|
1029
|
+
: { ...effective.config.runtimeProfiles };
|
|
1030
|
+
profilesMap[profileId] = mergedRecord;
|
|
1031
|
+
rawConfig.runtimeProfiles = profilesMap;
|
|
1032
|
+
// 8. Validate the full updated config
|
|
1033
|
+
const validation = validatePdConfig(rawConfig);
|
|
1034
|
+
if (!validation.ok) {
|
|
1035
|
+
return {
|
|
1036
|
+
ok: false,
|
|
1037
|
+
statusCode: 400,
|
|
1038
|
+
error: 'bad_request',
|
|
1039
|
+
message: `Profile validation failed: ${validation.errors.map(e => e.reason).join('; ')}`,
|
|
1040
|
+
nextAction: 'Fix profile fields and retry',
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
// 9. Atomic write
|
|
1044
|
+
try {
|
|
1045
|
+
writeConfigAtomic(configPath, rawConfig);
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1049
|
+
return { ok: false, statusCode: 500, error: 'write_error', message: `Failed to write config: ${message}`, nextAction: 'Check disk space and file permissions for .pd/config.yaml' };
|
|
1050
|
+
}
|
|
1051
|
+
// 10. Return the validated updated profile
|
|
1052
|
+
// Defensive guard: Object.hasOwn() doesn't narrow TS type.
|
|
1053
|
+
const updatedProfile = validation.value.runtimeProfiles[profileId];
|
|
1054
|
+
if (!updatedProfile) {
|
|
1055
|
+
return { ok: false, statusCode: 500, error: 'internal_error',
|
|
1056
|
+
message: `Profile '${profileId}' was updated but could not be read back from config.`,
|
|
1057
|
+
nextAction: 'This is a bug in PD config persistence. Please report it.' };
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
ok: true,
|
|
1061
|
+
profileId,
|
|
1062
|
+
profile: updatedProfile,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Delete a runtime profile from .pd/config.yaml.
|
|
1067
|
+
*
|
|
1068
|
+
* - Rejects non-existent profile (404 not_found)
|
|
1069
|
+
* - Rejects if profile is the defaultRuntime (400 bad_request)
|
|
1070
|
+
* - Rejects if any agent binding references the profile (400 bad_request, lists agents)
|
|
1071
|
+
* - Persists atomically, preserving unknown config sections
|
|
1072
|
+
*
|
|
1073
|
+
* ERR entries: same as createRuntimeProfile.
|
|
1074
|
+
*/
|
|
1075
|
+
export function deleteRuntimeProfile(workspaceDir, profileId) {
|
|
1076
|
+
// 1. Validate profileId
|
|
1077
|
+
if (typeof profileId !== 'string' || profileId.length === 0) {
|
|
1078
|
+
return { ok: false, statusCode: 400, error: 'bad_request', message: 'profileId must be a non-empty string' };
|
|
1079
|
+
}
|
|
1080
|
+
// 2. Load existing config — refuse to write on malformed (rc-9)
|
|
1081
|
+
const loadResult = loadPdConfig(workspaceDir);
|
|
1082
|
+
if (!loadResult.ok) {
|
|
1083
|
+
return {
|
|
1084
|
+
ok: false,
|
|
1085
|
+
statusCode: 409,
|
|
1086
|
+
error: 'conflict',
|
|
1087
|
+
message: `Cannot delete runtime profile: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${loadResult.errors.map(e => e.reason).join('; ')}`,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
const { effective } = loadResult;
|
|
1091
|
+
// 3. Reject if profile doesn't exist (404)
|
|
1092
|
+
if (!Object.hasOwn(effective.config.runtimeProfiles, profileId)) {
|
|
1093
|
+
return {
|
|
1094
|
+
ok: false,
|
|
1095
|
+
statusCode: 404,
|
|
1096
|
+
error: 'not_found',
|
|
1097
|
+
message: `Runtime profile '${profileId}' does not exist. Available profiles: ${Object.keys(effective.config.runtimeProfiles).join(', ')}`,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
// Capture the profile snapshot BEFORE deletion (for return value).
|
|
1101
|
+
// Defensive guard: Object.hasOwn() doesn't narrow TS type.
|
|
1102
|
+
const deletedProfile = effective.config.runtimeProfiles[profileId];
|
|
1103
|
+
if (!deletedProfile) {
|
|
1104
|
+
return { ok: false, statusCode: 500, error: 'internal_error',
|
|
1105
|
+
message: `Profile '${profileId}' existence check passed but value is undefined.`,
|
|
1106
|
+
nextAction: 'This is a bug in PD config resolution. Please report it.' };
|
|
1107
|
+
}
|
|
1108
|
+
// 4. Reject if profile is the defaultRuntime
|
|
1109
|
+
if (effective.config.internalAgents.defaultRuntime === profileId) {
|
|
1110
|
+
return {
|
|
1111
|
+
ok: false,
|
|
1112
|
+
statusCode: 400,
|
|
1113
|
+
error: 'bad_request',
|
|
1114
|
+
message: `Cannot delete runtime profile '${profileId}' because it is the default runtime. Change the default runtime first.`,
|
|
1115
|
+
nextAction: 'Use PATCH /api/v1/config/default-runtime to change the default to another profile before deleting',
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
// 5. Reject if any agent binding references this profile (list the agents)
|
|
1119
|
+
const referencingAgents = [];
|
|
1120
|
+
for (const agentName of INTERNAL_AGENT_NAMES) {
|
|
1121
|
+
const binding = effective.config.internalAgents.agents[agentName];
|
|
1122
|
+
if (binding && binding.runtimeProfile === profileId) {
|
|
1123
|
+
referencingAgents.push(agentName);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (referencingAgents.length > 0) {
|
|
1127
|
+
return {
|
|
1128
|
+
ok: false,
|
|
1129
|
+
statusCode: 400,
|
|
1130
|
+
error: 'bad_request',
|
|
1131
|
+
message: `Cannot delete runtime profile '${profileId}' because it is referenced by agents: ${referencingAgents.join(', ')}. Update their bindings first.`,
|
|
1132
|
+
nextAction: `Use PATCH /api/v1/config/agents/:agentName/binding to reassign: ${referencingAgents.join(', ')}`,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
// 6. Read raw config to preserve unknown sections, remove the profile
|
|
1136
|
+
const configPath = getPdConfigPath(workspaceDir);
|
|
1137
|
+
const rawConfig = readRawConfig(configPath);
|
|
1138
|
+
const profilesMap = isRecord(rawConfig.runtimeProfiles)
|
|
1139
|
+
? { ...rawConfig.runtimeProfiles }
|
|
1140
|
+
: {};
|
|
1141
|
+
delete profilesMap[profileId];
|
|
1142
|
+
rawConfig.runtimeProfiles = profilesMap;
|
|
1143
|
+
// 7. Validate the updated config
|
|
1144
|
+
const validation = validatePdConfig(rawConfig);
|
|
1145
|
+
if (!validation.ok) {
|
|
1146
|
+
return {
|
|
1147
|
+
ok: false,
|
|
1148
|
+
statusCode: 400,
|
|
1149
|
+
error: 'bad_request',
|
|
1150
|
+
message: `Updated config would be invalid after deletion: ${validation.errors.map(e => e.reason).join('; ')}`,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
// 8. Atomic write
|
|
1154
|
+
try {
|
|
1155
|
+
writeConfigAtomic(configPath, rawConfig);
|
|
1156
|
+
}
|
|
1157
|
+
catch (err) {
|
|
1158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1159
|
+
return { ok: false, statusCode: 500, error: 'write_error', message: `Failed to write config: ${message}`, nextAction: 'Check disk space and file permissions for .pd/config.yaml' };
|
|
1160
|
+
}
|
|
1161
|
+
// 9. Return the deleted profile snapshot for client confirmation
|
|
1162
|
+
return {
|
|
1163
|
+
ok: true,
|
|
1164
|
+
profileId,
|
|
1165
|
+
profile: deletedProfile,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
805
1168
|
// ── Check Readiness ──────────────────────────────────────────────────────────
|
|
806
1169
|
export function checkReadiness(workspaceDir, agentName, getEnvVar = (name) => process.env[name]) {
|
|
807
1170
|
// 1. Validate agent name
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* even when the rulecode_context_v2 flag is off.
|
|
9
9
|
*/
|
|
10
10
|
export interface ActivationRecord {
|
|
11
|
-
|
|
11
|
+
activationId: string;
|
|
12
12
|
artifactId: string;
|
|
13
13
|
principleId: string;
|
|
14
14
|
channel: string;
|
|
@@ -46,9 +46,12 @@ export interface ActivationRecord {
|
|
|
46
46
|
}
|
|
47
47
|
export interface ActivationsResponse {
|
|
48
48
|
activations: ActivationRecord[];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
/** 'ok' on success, 'degraded' when data is incomplete/missing. */
|
|
50
|
+
status: 'ok' | 'degraded';
|
|
51
|
+
/** Present when status is 'degraded' — explains why. */
|
|
52
|
+
reason?: string;
|
|
53
|
+
/** Present when status is 'degraded' — next operator action. */
|
|
54
|
+
nextAction?: string;
|
|
52
55
|
}
|
|
53
56
|
export declare class ActivationsConsoleModel {
|
|
54
57
|
private readonly workspaceDir;
|
|
@@ -42,7 +42,7 @@ export class ActivationsConsoleModel {
|
|
|
42
42
|
async getActivations() {
|
|
43
43
|
const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
|
|
44
44
|
if (!fs.existsSync(stateDbPath)) {
|
|
45
|
-
return { activations: [],
|
|
45
|
+
return { activations: [], status: 'degraded', reason: 'state.db not found — workspace may not be initialized', nextAction: 'Run pd runtime diagnostics to check workspace state' };
|
|
46
46
|
}
|
|
47
47
|
const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
|
|
48
48
|
try {
|
|
@@ -54,7 +54,7 @@ export class ActivationsConsoleModel {
|
|
|
54
54
|
}
|
|
55
55
|
catch (err) {
|
|
56
56
|
if (isMissingTableError(err)) {
|
|
57
|
-
return { activations: [],
|
|
57
|
+
return { activations: [], status: 'degraded', reason: 'activation table not found — workspace may not be initialized', nextAction: 'Run pd runtime internalization integrity to check schema' };
|
|
58
58
|
}
|
|
59
59
|
throw err;
|
|
60
60
|
}
|
|
@@ -156,7 +156,7 @@ export class ActivationsConsoleModel {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
const enriched = {
|
|
159
|
-
|
|
159
|
+
activationId: record.activationId,
|
|
160
160
|
artifactId: record.artifactId,
|
|
161
161
|
principleId: artifactPrincipleMap.get(record.artifactId) ?? 'unlinked',
|
|
162
162
|
channel: record.channel,
|
|
@@ -181,13 +181,14 @@ export class ActivationsConsoleModel {
|
|
|
181
181
|
if (danglingArtifactIds.size > 0) {
|
|
182
182
|
return {
|
|
183
183
|
activations: facts,
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
status: 'degraded',
|
|
185
|
+
reason: `${danglingArtifactIds.size} activation(s) reference non-existent artifact_id(s): ${Array.from(danglingArtifactIds).join(', ')}`,
|
|
186
|
+
nextAction: 'Run pd runtime internalization integrity to check for orphaned activations',
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
return {
|
|
189
190
|
activations: facts,
|
|
190
|
-
|
|
191
|
+
status: 'ok',
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
finally {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* - ERR-014/ERR-016/ERR-017: Safe serialization for previews
|
|
21
21
|
* - ERR-045: ANY-segment redaction for sensitive keys
|
|
22
22
|
*/
|
|
23
|
-
import { sendSuccess, sendError, sendNotFound, sendMethodNotAllowed, sendBadRequest } from '../utils/response.js';
|
|
24
|
-
import { getConfigSummary, getConfigCatalog, updateAgentBinding, updateDefaultRuntime, checkReadiness, getPrinciplesOutputLanguage, updatePrinciplesOutputLanguage, updateFeatureFlag, } from '../config/pd-config-store.js';
|
|
23
|
+
import { sendSuccess, sendError, sendNotFound, sendMethodNotAllowed, sendBadRequest, sendJson } from '../utils/response.js';
|
|
24
|
+
import { getConfigSummary, getConfigCatalog, updateAgentBinding, updateDefaultRuntime, checkReadiness, getPrinciplesOutputLanguage, updatePrinciplesOutputLanguage, updateFeatureFlag, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, } from '../config/pd-config-store.js';
|
|
25
25
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
26
26
|
const MAX_BODY_SIZE = 1024 * 64; // 64 KB — generous for config binding updates
|
|
27
27
|
// ── Request Body Reader ──────────────────────────────────────────────────────
|
|
@@ -262,6 +262,96 @@ export async function handleConfigRoute(req, res, ctx) {
|
|
|
262
262
|
sendMethodNotAllowed(res);
|
|
263
263
|
return;
|
|
264
264
|
}
|
|
265
|
+
// POST /profiles — create a new runtime profile
|
|
266
|
+
if (subPath === '/profiles') {
|
|
267
|
+
if (method !== 'POST') {
|
|
268
|
+
sendMethodNotAllowed(res);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
let bodyText;
|
|
272
|
+
try {
|
|
273
|
+
bodyText = await readBody(req);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
sendBadRequest(res, 'Request body exceeds maximum allowed size');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const body = safeParseBody(bodyText);
|
|
280
|
+
if (body === null) {
|
|
281
|
+
sendBadRequest(res, 'Invalid JSON body');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Validate body shape: { id: string, profile: object } (ERR-001: no `as` bypass)
|
|
285
|
+
if (!isObject(body)) {
|
|
286
|
+
sendBadRequest(res, 'Body must be a JSON object with id and profile fields');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!Object.hasOwn(body, 'id')) {
|
|
290
|
+
sendBadRequest(res, 'Missing required field: id');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (typeof body.id !== 'string' || body.id.length === 0) {
|
|
294
|
+
sendBadRequest(res, 'id must be a non-empty string');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!Object.hasOwn(body, 'profile')) {
|
|
298
|
+
sendBadRequest(res, 'Missing required field: profile');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const result = createRuntimeProfile(workspaceDir, body.id, body.profile);
|
|
302
|
+
if (!result.ok) {
|
|
303
|
+
sendError(res, result.statusCode, result.error, result.message);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// 201 Created for new resource
|
|
307
|
+
sendJson(res, 201, { success: true, data: { profileId: result.profileId, profile: result.profile } });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// PATCH/DELETE /profiles/:profileId — update or delete a runtime profile
|
|
311
|
+
const profileMatch = /^\/profiles\/([^/]+)$/.exec(subPath);
|
|
312
|
+
if (profileMatch) {
|
|
313
|
+
let profileId;
|
|
314
|
+
try {
|
|
315
|
+
profileId = decodeURIComponent(profileMatch[1] ?? '');
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
sendBadRequest(res, 'Invalid profile ID encoding');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (method === 'PATCH') {
|
|
322
|
+
let bodyText;
|
|
323
|
+
try {
|
|
324
|
+
bodyText = await readBody(req);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
sendBadRequest(res, 'Request body exceeds maximum allowed size');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const body = safeParseBody(bodyText);
|
|
331
|
+
if (body === null) {
|
|
332
|
+
sendBadRequest(res, 'Invalid JSON body');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const result = updateRuntimeProfile(workspaceDir, profileId, body);
|
|
336
|
+
if (!result.ok) {
|
|
337
|
+
sendError(res, result.statusCode, result.error, result.message);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
sendSuccess(res, { profileId: result.profileId, profile: result.profile });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (method === 'DELETE') {
|
|
344
|
+
const result = deleteRuntimeProfile(workspaceDir, profileId);
|
|
345
|
+
if (!result.ok) {
|
|
346
|
+
sendError(res, result.statusCode, result.error, result.message);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
sendSuccess(res, { profileId: result.profileId, profile: result.profile });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
sendMethodNotAllowed(res);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
265
355
|
// 404 fallback
|
|
266
356
|
sendNotFound(res, `Route /api/v1/config${subPath} not found`);
|
|
267
357
|
}
|