@dsh-enhanced/assistant-goals 0.1.67 → 0.1.70

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/lib/service.js CHANGED
@@ -17,11 +17,17 @@ import { buildGoalStrategyHistory } from './strategy-feedback.js';
17
17
  import { failureSummaryEvidenceDigest, OwnerVerifiedWorkflowSourceError, ownerGoalRunProof, verifiedWorkflowSource, verifiedWorkflowSourceChain } from './verified-workflow.js';
18
18
  import { buildOwnerAcceptedStepArtifacts, buildOwnerVerifiedArtifacts, validateOwnerVerifiedArtifactsInput } from './verified-artifact.js';
19
19
  import { assertGoalDependenciesAchieved, goalDependencies } from './dependency.js';
20
+ import { buildGoalOwnerIntent, ownerIntentObjectiveSpan } from './owner-intent.js';
21
+ import { GoalOwnerIntentStore, goalOwnerIntentSupplier } from './owner-intent-store.js';
22
+ import { validateOwnerAcceptanceSeal, validateOwnerPreparedAcceptanceInput, resolveOwnerPreparedAcceptanceInput, preparedRegistration } from './owner-acceptance.js';
23
+ import { GoalOwnerAcceptanceStore } from './owner-acceptance-store.js';
20
24
  export const Config = Schema.object({
21
25
  databasePath: Schema.string().default(join(homedir(), '.dsh', 'assistant-goals.sqlite')),
22
26
  preauthorizedCreateMaxRounds: Schema.number().step(1).min(0).max(32).default(0),
23
27
  preauthorizedSchedule: Schema.boolean().default(false),
24
28
  eventWaits: Schema.boolean().default(false),
29
+ ownerIntent: Schema.boolean().default(false),
30
+ ownerPreparedAcceptance: Schema.boolean().default(false),
25
31
  maxContextChars: Schema.number().step(1).min(1024).max(65536).default(12000),
26
32
  verifyNativeRounds: Schema.boolean().default(false),
27
33
  verifyGoalOutcome: Schema.boolean().default(false),
@@ -280,6 +286,7 @@ export class AssistantGoalsService extends Service {
280
286
  #active = true;
281
287
  #maxChars;
282
288
  #createMaxRounds;
289
+ #stepMaxDurationMs = 60_000;
283
290
  #preauthorizedSchedule = false;
284
291
  preauthorizedCreateEnabled;
285
292
  preauthorizedScheduleEnabled;
@@ -291,8 +298,17 @@ export class AssistantGoalsService extends Service {
291
298
  #wake;
292
299
  #outcome;
293
300
  #eventWait;
301
+ /** Set only when owner intent freezing is enabled; every Goal still needs its own frozen proof. */
302
+ #ownerIntent;
303
+ /** Set only when Host-prepared owner acceptance is enabled; every goal needs its own seal. */
304
+ #ownerAcceptance;
305
+ /** The current Host producer registration; seal requests are rejected without it. */
306
+ #acceptanceRegistration;
307
+ /** Native create emits goal/changed synchronously; this marker defers legacy binding. */
308
+ #preparingPreparedCreation = false;
294
309
  #ownerRepairBindings = new Map();
295
310
  eventWaitsEnabled;
311
+ ownerAcceptanceEnabled;
296
312
  #ownerGoalOutcomeFeedbackTargets = new WeakMap();
297
313
  constructor(ctx, input = {}) {
298
314
  super(ctx, 'assistantGoals');
@@ -308,6 +324,7 @@ export class AssistantGoalsService extends Service {
308
324
  const duration = input.stepMaxDurationMs ?? 60000;
309
325
  if (!Number.isSafeInteger(duration) || duration < 1 || duration > 300000 || (input.verifyNativeRounds !== undefined && typeof input.verifyNativeRounds !== 'boolean'))
310
326
  throw new Error('assistant-goals: invalid execution limits');
327
+ this.#stepMaxDurationMs = duration;
311
328
  const budget = input.executionBudget === undefined ? undefined : validateGoalBudgetConfig(input.executionBudget);
312
329
  if (budget !== undefined && input.verifyNativeRounds !== true)
313
330
  throw new Error('assistant-goals: execution budget requires verified native rounds');
@@ -334,9 +351,49 @@ export class AssistantGoalsService extends Service {
334
351
  Object.defineProperty(this, 'eventWaitsEnabled', { value: input.eventWaits === true, enumerable: true, writable: false, configurable: false });
335
352
  if (this.eventWaitsEnabled && (wake === undefined || budget === undefined || path === ':memory:' || input.verifyGoalOutcome !== true))
336
353
  throw new Error('assistant-goals: event waits require durable wake, budgets and verified outcomes');
354
+ if (input.ownerIntent !== undefined && typeof input.ownerIntent !== 'boolean')
355
+ throw new Error('assistant-goals: ownerIntent must be boolean');
356
+ const ownerIntent = input.ownerIntent === true;
357
+ // The side ledger must be a durable private file: an in-memory goal database
358
+ // cannot prove its intent ledger survived the exact owner request.
359
+ if (ownerIntent && path === ':memory:')
360
+ throw new Error('assistant-goals: owner intent requires a durable private database file');
361
+ if (input.ownerPreparedAcceptance !== undefined && typeof input.ownerPreparedAcceptance !== 'boolean')
362
+ throw new Error('assistant-goals: ownerPreparedAcceptance must be boolean');
363
+ const ownerPreparedAcceptance = input.ownerPreparedAcceptance === true;
364
+ if (ownerPreparedAcceptance && path === ':memory:')
365
+ throw new Error('assistant-goals: owner prepared acceptance requires a durable private database file');
366
+ Object.defineProperty(this, 'ownerAcceptanceEnabled', { value: ownerPreparedAcceptance, enumerable: true, writable: false, configurable: false });
337
367
  this.#store = new GoalStore(path);
338
- ctx.effect(() => () => { this.#active = false; for (const binding of this.#ownerRepairBindings.values())
339
- binding.dispose(); this.#ownerRepairBindings.clear(); this.#store.close(); }, 'assistant-goals.store');
368
+ if (ownerIntent) {
369
+ try {
370
+ this.#ownerIntent = { store: new GoalOwnerIntentStore(`${path}.owner-intents`) };
371
+ }
372
+ catch (error) {
373
+ this.#store.close();
374
+ throw error;
375
+ }
376
+ }
377
+ if (ownerPreparedAcceptance) {
378
+ try {
379
+ this.#ownerAcceptance = { store: new GoalOwnerAcceptanceStore(`${path}.owner-acceptance`) };
380
+ }
381
+ catch (error) {
382
+ this.#ownerIntent?.store.close();
383
+ this.#store.close();
384
+ throw error;
385
+ }
386
+ }
387
+ ctx.effect(() => () => {
388
+ this.#active = false;
389
+ this.#acceptanceRegistration = undefined;
390
+ for (const binding of this.#ownerRepairBindings.values())
391
+ binding.dispose();
392
+ this.#ownerRepairBindings.clear();
393
+ this.#store.close();
394
+ this.#ownerIntent?.store.close();
395
+ this.#ownerAcceptance?.store.close();
396
+ }, 'assistant-goals.store');
340
397
  this.#execution = new GoalExecutionRuntime(ctx, input.verifyNativeRounds === true ? (path === ':memory:' ? path : `${path}.executions`) : undefined, duration, (agent, consume = true) => {
341
398
  const scope = this.#scope(agent, 'execute', consume);
342
399
  const record = this.#observe(agent, false, consume);
@@ -346,7 +403,7 @@ export class AssistantGoalsService extends Service {
346
403
  }, input.verifyGoalOutcome === true ? {
347
404
  prepare: (agent, run) => this.#outcome.prepare(agent, run),
348
405
  settled: (agent, run, assertCurrent) => this.#outcome.settled(agent, run, assertCurrent),
349
- } : undefined, record => this.#assertDependencies(record));
406
+ } : undefined, record => this.#assertDependencies(record), () => this.#ownerAcceptance !== undefined, record => this.#ownerAcceptance === undefined ? undefined : this.#sealFor(record));
350
407
  if (input.verifyGoalOutcome === true)
351
408
  this.#outcome = new GoalOutcomeRuntime(ctx, `${path}.outcomes`, agent => {
352
409
  this.#scope(agent, 'execute');
@@ -355,7 +412,7 @@ export class AssistantGoalsService extends Service {
355
412
  throw new Error('assistant-goals: current whole-goal definition required');
356
413
  return record;
357
414
  }, this.#execution.list, duration, record => this.#assertDependencies(record), (record, run, agent) => this.#eventWait?.acceptsPausedOutcomeSettlement(record, run) === true
358
- && this.#execution.acceptsPausedEventWaitSettlement(record, run, agent), (record, wake, run) => this.#eventWait?.acceptsHistoricalPausedOutcomeCompletion(record, wake, run) === true);
415
+ && this.#execution.acceptsPausedEventWaitSettlement(record, run, agent), (record, wake, run) => this.#eventWait?.acceptsHistoricalPausedOutcomeCompletion(record, wake, run) === true, () => this.#ownerAcceptance !== undefined, record => this.#ownerAcceptance === undefined ? undefined : this.#sealFor(record));
359
416
  if (this.#outcome !== undefined)
360
417
  ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
361
418
  signal.throwIfAborted();
@@ -366,7 +423,7 @@ export class AssistantGoalsService extends Service {
366
423
  return await next();
367
424
  });
368
425
  if (budget !== undefined)
369
- this.#budget = new GoalBudgetRuntime(ctx, path === ':memory:' ? path : `${path}.budgets`, budget, this.#execution.budgetState);
426
+ this.#budget = new GoalBudgetRuntime(ctx, path === ':memory:' ? path : `${path}.budgets`, budget, this.#execution.budgetState, record => goalOwnerIntentSupplier(this.#ownerIntent?.store, record));
370
427
  if (strategy)
371
428
  ctx.inject(['subagents'], runtime => {
372
429
  const instance = new GoalStrategyRuntime(runtime, `${path}.strategies`, strategy, {
@@ -438,7 +495,8 @@ export class AssistantGoalsService extends Service {
438
495
  this.#clear(agent, change.ref.id, change.ref.revision);
439
496
  else {
440
497
  const record = this.#observe(agent, change.operation === 'create');
441
- if (record !== undefined && (change.operation === 'create' || change.operation === 'edit'))
498
+ if (record !== undefined && !this.#preparingPreparedCreation
499
+ && (change.operation === 'create' || change.operation === 'edit'))
442
500
  this.#bindOutcome(agent, record);
443
501
  }
444
502
  this.#eventWait?.reconcile();
@@ -515,6 +573,15 @@ export class AssistantGoalsService extends Service {
515
573
  #bindOutcome(agent, record) {
516
574
  if (this.#outcome === undefined)
517
575
  return;
576
+ if (this.#ownerAcceptance !== undefined) {
577
+ // Prepared acceptance is sealed by the Host after the business goal
578
+ // exists; never silently fall back to the legacy profile binding here.
579
+ const seal = this.#sealFor(record);
580
+ if (seal === undefined)
581
+ return;
582
+ this.#outcome.bindPrepared(record, seal);
583
+ return;
584
+ }
518
585
  this.#requireOwnerTurn(agent, record.scope);
519
586
  this.#outcome.bind(record);
520
587
  }
@@ -694,6 +761,10 @@ export class AssistantGoalsService extends Service {
694
761
  const scope = this.#scope(execution.agent, 'create', false);
695
762
  this.#scope(execution.agent, 'observe', false);
696
763
  this.#requireOwnerTurn(execution.agent, scope);
764
+ // Preauthorization is a read-only prediction of the same frozen proof the
765
+ // real execution will require; it never writes or consumes a budget.
766
+ if (this.#ownerIntent !== undefined)
767
+ this.#freezeOwnerIntent(execution.agent, execution, objective, scope);
697
768
  this.#outcome.preflight(scope, objective);
698
769
  const verifier = this.ctx.get('assistantVerifier', false);
699
770
  return ['goal-step', 'goal-outcome'].every(taskKind => {
@@ -728,7 +799,15 @@ export class AssistantGoalsService extends Service {
728
799
  this.#outcome?.preflight(scope, objective.trim());
729
800
  // Delivery retains its real source kind. Its current owner-turn proof is the
730
801
  // Host authority for this bridge; never forge a native direct-user event.
731
- native.create(agent, { objective, ...(maxGoalRounds === undefined ? {} : { maxGoalRounds }) });
802
+ // Native create emits goal/changed synchronously; prepared mode must not
803
+ // attempt its legacy whole-goal binding before the Host seals acceptance.
804
+ this.#preparingPreparedCreation = this.#ownerAcceptance !== undefined;
805
+ try {
806
+ native.create(agent, { objective, ...(maxGoalRounds === undefined ? {} : { maxGoalRounds }) });
807
+ }
808
+ finally {
809
+ this.#preparingPreparedCreation = false;
810
+ }
732
811
  try {
733
812
  const record = this.#observe(agent, true);
734
813
  if (record !== undefined) {
@@ -741,6 +820,334 @@ export class AssistantGoalsService extends Service {
741
820
  ? 'assistant-goals: native goal created but context could not be indexed; inspect the current native goal before retrying'
742
821
  : 'assistant-goals: native goal created but context or whole-goal acceptance is unavailable; inspect the current native goal and exact acceptance profile before retrying');
743
822
  };
823
+ /**
824
+ * Create one goal from the live authenticated owner tool call after freezing
825
+ * the real owner text, supplier and call identity. Disabled (undefined
826
+ * ledger) keeps the historical create path; enabled means every native goal
827
+ * creation requires a fresh Delivery proof and fails closed otherwise.
828
+ */
829
+ createFromOwnerIntent = (execution, objective, maxGoalRounds, acceptance) => {
830
+ if (this.#ownerAcceptance !== undefined)
831
+ return this.#createFromOwnerIntentPrepared(execution, objective, maxGoalRounds, acceptance);
832
+ if (acceptance !== undefined)
833
+ throw new Error('assistant-goals: owner-prepared acceptance is not enabled; omit acceptance or enable ownerPreparedAcceptance');
834
+ if (this.#ownerIntent === undefined)
835
+ return this.create(execution.agent, objective, maxGoalRounds);
836
+ const agent = execution.agent;
837
+ if (agent === undefined)
838
+ throw new Error('assistant-goals: owner intent requires the live owner tool call');
839
+ if (this.#createMaxRounds > 0) {
840
+ maxGoalRounds ??= this.#createMaxRounds;
841
+ if (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1 || maxGoalRounds > this.#createMaxRounds)
842
+ throw new Error('assistant-goals: configured creation round limit exceeded');
843
+ }
844
+ const scope = this.#scope(agent, 'create');
845
+ this.#scope(agent, 'observe');
846
+ this.#requireOwnerTurn(agent, scope);
847
+ if (typeof objective !== 'string' || objective.trim().length === 0 || objective.length > 16384)
848
+ throw new Error('assistant-goals: invalid objective');
849
+ if (maxGoalRounds !== undefined && (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1))
850
+ throw new Error('assistant-goals: invalid round limit');
851
+ const intent = this.#freezeOwnerIntent(agent, execution, objective, scope);
852
+ const native = this.ctx.get('goals');
853
+ if (native === undefined)
854
+ throw new Error('assistant-goals: native goal service unavailable');
855
+ // Match the native domain's normalization before looking up exact profiles.
856
+ this.#outcome?.preflight(scope, objective.trim());
857
+ this.#preparingPreparedCreation = this.#ownerAcceptance !== undefined;
858
+ try {
859
+ native.create(agent, { objective, ...(maxGoalRounds === undefined ? {} : { maxGoalRounds }) });
860
+ }
861
+ finally {
862
+ this.#preparingPreparedCreation = false;
863
+ }
864
+ let record;
865
+ try {
866
+ record = this.#observe(agent, true);
867
+ if (record !== undefined)
868
+ this.#bindOutcome(agent, record);
869
+ }
870
+ catch {
871
+ record = undefined;
872
+ }
873
+ if (record === undefined) {
874
+ // Never attach the frozen intent to a goal we could not read back.
875
+ throw new Error(this.#outcome === undefined
876
+ ? 'assistant-goals: native goal created but context could not be indexed; inspect the current native goal before retrying'
877
+ : 'assistant-goals: native goal created but context or whole-goal acceptance is unavailable; inspect the current native goal and exact acceptance profile before retrying');
878
+ }
879
+ try {
880
+ this.#ownerIntent.store.record(intent, record.id, record.definition.digest);
881
+ }
882
+ catch (error) {
883
+ // The native goal must never survive without its frozen proof. Roll it
884
+ // back and report which outcome the caller must inspect.
885
+ const rolledBack = this.#rollbackCreatedGoal(agent, record);
886
+ throw new Error(rolledBack
887
+ ? 'assistant-goals: native goal created but the owner intent could not be recorded; the native goal was rolled back'
888
+ : 'assistant-goals: native goal created without a frozen owner intent; inspect and clear it before use', { cause: error });
889
+ }
890
+ return record;
891
+ };
892
+ /** Owner-turn creation that also seals the protected acceptance before returning. */
893
+ #createFromOwnerIntentPrepared(execution, objective, maxGoalRounds, acceptance) {
894
+ const agent = execution.agent;
895
+ if (agent === undefined)
896
+ throw new Error('assistant-goals: owner intent requires the live owner tool call');
897
+ if (acceptance === undefined)
898
+ throw new Error('assistant-goals: owner-prepared acceptance requires explicit criteria');
899
+ const input = resolveOwnerPreparedAcceptanceInput(acceptance, this.#stepMaxDurationMs);
900
+ // Fail before native creation when the verifier side cannot seal at all.
901
+ this.#sealCapability();
902
+ if (this.#createMaxRounds > 0) {
903
+ maxGoalRounds ??= this.#createMaxRounds;
904
+ if (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1 || maxGoalRounds > this.#createMaxRounds)
905
+ throw new Error('assistant-goals: configured creation round limit exceeded');
906
+ }
907
+ const scope = this.#scope(agent, 'create');
908
+ this.#scope(agent, 'observe');
909
+ this.#requireOwnerTurn(agent, scope);
910
+ if (typeof objective !== 'string' || objective.trim().length === 0 || objective.length > 16384)
911
+ throw new Error('assistant-goals: invalid objective');
912
+ if (maxGoalRounds !== undefined && (!Number.isSafeInteger(maxGoalRounds) || maxGoalRounds < 1))
913
+ throw new Error('assistant-goals: invalid round limit');
914
+ const intent = this.#ownerIntent === undefined ? undefined : this.#freezeOwnerIntent(agent, execution, objective, scope);
915
+ const native = this.ctx.get('goals');
916
+ if (native === undefined)
917
+ throw new Error('assistant-goals: native goal service unavailable');
918
+ // Match the native domain's normalization before looking up exact profiles.
919
+ this.#outcome?.preflight(scope, objective.trim());
920
+ this.#preparingPreparedCreation = true;
921
+ try {
922
+ native.create(agent, { objective, ...(maxGoalRounds === undefined ? {} : { maxGoalRounds }) });
923
+ }
924
+ finally {
925
+ this.#preparingPreparedCreation = false;
926
+ }
927
+ let record;
928
+ try {
929
+ record = this.#observe(agent, true);
930
+ }
931
+ catch {
932
+ record = undefined;
933
+ }
934
+ if (record === undefined) {
935
+ const rolledBack = this.#rollbackCurrentGoal(agent);
936
+ throw new Error(rolledBack
937
+ ? 'assistant-goals: native goal created but context could not be indexed; the native goal was rolled back'
938
+ : 'assistant-goals: native goal created but context could not be indexed; inspect and clear the current native goal before retrying');
939
+ }
940
+ try {
941
+ this.#sealOwnerAcceptanceRecord(record, input);
942
+ }
943
+ catch (error) {
944
+ const rolledBack = this.#rollbackCreatedGoal(agent, record);
945
+ throw new Error(rolledBack
946
+ ? 'assistant-goals: native goal created but the owner acceptance could not be sealed; the native goal was rolled back'
947
+ : 'assistant-goals: native goal created without a sealed owner acceptance; inspect and clear it before use', { cause: error });
948
+ }
949
+ if (intent !== undefined) {
950
+ try {
951
+ this.#ownerIntent.store.record(intent, record.id, record.definition.digest);
952
+ }
953
+ catch (error) {
954
+ const rolledBack = this.#rollbackCreatedGoal(agent, record);
955
+ throw new Error(rolledBack
956
+ ? 'assistant-goals: native goal created but the owner intent could not be recorded; the native goal was rolled back'
957
+ : 'assistant-goals: native goal created without a frozen owner intent; inspect and clear it before use', { cause: error });
958
+ }
959
+ }
960
+ return record;
961
+ }
962
+ /** Clear a just-created native goal when the business record could not be observed. */
963
+ #rollbackCurrentGoal(agent) {
964
+ try {
965
+ const native = this.ctx.get('goals');
966
+ if (native === undefined)
967
+ return false;
968
+ const current = native.get(agent);
969
+ if (current === undefined)
970
+ return true;
971
+ native.clear(agent, { id: current.id, revision: current.revision });
972
+ return native.get(agent) === undefined;
973
+ }
974
+ catch {
975
+ return false;
976
+ }
977
+ }
978
+ #freezeOwnerIntent(agent, execution, objective, scope) {
979
+ const delivery = this.ctx.get('assistantDelivery');
980
+ if (delivery === undefined || typeof delivery.inspectOwnerForegroundToolCallForAgent !== 'function'
981
+ || typeof delivery.readOwnerForegroundToolInputForAgent !== 'function')
982
+ throw new Error('assistant-goals: owner intent proof is unavailable');
983
+ const intent = buildGoalOwnerIntent({ delivery, agent, execution, scope });
984
+ // The model selects only words already present verbatim in the owner input;
985
+ // the frozen span must be exactly the span of the live objective argument.
986
+ const span = ownerIntentObjectiveSpan(intent.text, objective);
987
+ if (objective !== intent.objective.text || span.start !== intent.objective.start || span.end !== intent.objective.end) {
988
+ throw new Error("assistant-goals: objective must quote the owner's original words verbatim");
989
+ }
990
+ return intent;
991
+ }
992
+ /**
993
+ * Read-only catalog of installed acceptance authorities for the caller's
994
+ * owner scope. The verifier keeps the authority list; Goals only proxies it
995
+ * inside a model tool that is registered in owner-prepared mode.
996
+ */
997
+ inspectAcceptanceAuthorities = (agent) => {
998
+ const scope = this.#scope(agent, 'inspect');
999
+ const verifier = this.ctx.get('assistantVerifier', false);
1000
+ if (verifier === undefined || typeof verifier.inspectAcceptanceAuthorities !== 'function') {
1001
+ throw new Error('assistant-goals: acceptance authority catalog is unavailable; the verifier peer does not expose it');
1002
+ }
1003
+ return verifier.inspectAcceptanceAuthorities({ scope: { workspace: scope.workspace, preset: scope.preset },
1004
+ owner: { principalRecordId: scope.principalRecordId, principalVersion: scope.principalVersion } });
1005
+ };
1006
+ /**
1007
+ * Host-only read of the frozen owner intent for one owner-scoped business
1008
+ * goal. A stored row whose definition digest no longer matches the current
1009
+ * goal definition is a stale binding and returns undefined instead of
1010
+ * silently authorizing a different objective.
1011
+ */
1012
+ ownerIntent = (agent, goalId) => {
1013
+ const store = this.#ownerIntent?.store;
1014
+ if (store === undefined)
1015
+ return undefined;
1016
+ const scope = this.#scope(agent, 'inspect');
1017
+ const record = this.#store.get(scope, goalId);
1018
+ if (record === undefined)
1019
+ return undefined;
1020
+ const stored = store.inspect(goalId);
1021
+ if (stored === undefined || stored.definitionDigest !== record.definition.digest)
1022
+ return undefined;
1023
+ return stored.intent;
1024
+ };
1025
+ /** Best-effort clear of a just-created native goal whose proof could not be frozen. */
1026
+ #rollbackCreatedGoal(agent, record) {
1027
+ try {
1028
+ const native = this.ctx.get('goals');
1029
+ if (native === undefined)
1030
+ return false;
1031
+ const current = native.get(agent);
1032
+ if (current === undefined)
1033
+ return true;
1034
+ if (String(current.id) !== record.native.goalId)
1035
+ return false;
1036
+ native.clear(agent, { id: current.id, revision: current.revision });
1037
+ return native.get(agent) === undefined;
1038
+ }
1039
+ catch {
1040
+ return false;
1041
+ }
1042
+ }
1043
+ /** Current seal for one business goal; a stale definition digest fails closed. */
1044
+ #sealFor(record) {
1045
+ const store = this.#ownerAcceptance?.store;
1046
+ if (store === undefined)
1047
+ return undefined;
1048
+ const seal = store.get(record.id);
1049
+ if (seal === undefined)
1050
+ return undefined;
1051
+ if (seal.definition.version !== record.definition.version || seal.definition.digest !== record.definition.digest) {
1052
+ throw new Error('assistant-goals: sealed owner acceptance is stale for the current goal definition; create a new goal');
1053
+ }
1054
+ return seal;
1055
+ }
1056
+ /**
1057
+ * Host-only: seal the protected acceptance for one owner-scoped business goal
1058
+ * before its first native round. Criteria are Host-prepared and never reachable
1059
+ * from model tool arguments; a goal in prepared mode cannot run unsealed.
1060
+ */
1061
+ sealOwnerAcceptance = (agent, goalId, value) => {
1062
+ const scope = this.#scope(agent, 'create');
1063
+ const record = this.#store.get(scope, goalId);
1064
+ if (record === undefined)
1065
+ throw new Error('assistant-goals: goal not found');
1066
+ if (record.native.phase !== 'active' && record.native.phase !== 'paused')
1067
+ throw new Error('assistant-goals: only an active or paused goal can seal owner acceptance');
1068
+ const input = validateOwnerPreparedAcceptanceInput(value, this.#stepMaxDurationMs);
1069
+ const capability = this.#sealCapability();
1070
+ const stepCriteria = input.stepCriteria ?? input.criteria;
1071
+ const existing = capability.store.get(record.id);
1072
+ if (existing !== undefined) {
1073
+ const same = existing.definition.version === record.definition.version && existing.definition.digest === record.definition.digest
1074
+ && acceptanceDigest(existing.criteria) === acceptanceDigest(input.criteria)
1075
+ && acceptanceDigest(existing.stepCriteria) === acceptanceDigest(stepCriteria)
1076
+ && acceptanceDigest(existing.bounds) === acceptanceDigest(input.bounds)
1077
+ && existing.validityMs === input.validityMs && acceptanceDigest(existing.preparer) === acceptanceDigest(input.preparer);
1078
+ if (!same)
1079
+ throw new Error('assistant-goals: owner acceptance is already sealed for this goal with different criteria; create a new goal to change it');
1080
+ this.#outcome.bindPrepared(record, existing);
1081
+ return this.#sealView(existing);
1082
+ }
1083
+ try {
1084
+ return this.#sealView(this.#sealOwnerAcceptanceRecord(record, input));
1085
+ }
1086
+ catch (error) {
1087
+ // An unsealed goal cannot run in prepared mode; never leave it silently usable.
1088
+ const rolledBack = this.#rollbackCreatedGoal(agent, record);
1089
+ throw new Error(rolledBack
1090
+ ? 'assistant-goals: owner acceptance could not be sealed; the native goal was rolled back'
1091
+ : 'assistant-goals: owner acceptance could not be sealed; the native goal remains without a sealed owner acceptance and must be inspected and cleared before use', { cause: error });
1092
+ }
1093
+ };
1094
+ #sealView(seal) {
1095
+ return Object.freeze({ goalId: seal.goalId, definitionDigest: seal.definition.digest,
1096
+ outcome: Object.freeze({ contractId: seal.outcome.contractId, contractDigest: seal.outcome.contractDigest }),
1097
+ expiresAt: seal.outcomeExpiresAt });
1098
+ }
1099
+ /** Resolve the live verifier capability required to seal prepared acceptance. */
1100
+ #sealCapability() {
1101
+ const store = this.#ownerAcceptance?.store;
1102
+ if (store === undefined)
1103
+ throw new Error('assistant-goals: owner prepared acceptance is not enabled');
1104
+ const registration = this.#acceptanceRegistration;
1105
+ if (registration === undefined)
1106
+ throw new Error('assistant-goals: owner prepared acceptance requires the goal verifier registration');
1107
+ if (this.#outcome === undefined)
1108
+ throw new Error('assistant-goals: owner prepared acceptance requires whole-goal verification');
1109
+ const verifier = this.ctx.get('assistantVerifier', false);
1110
+ const prepared = preparedRegistration(registration);
1111
+ if (prepared === undefined)
1112
+ throw new Error('assistant-goals: the goal verifier does not support owner-prepared acceptance');
1113
+ return { store, prepared, verifier };
1114
+ }
1115
+ /** Seal one prepared outcome contract and persist it before the first round. */
1116
+ #sealOwnerAcceptanceRecord(record, input) {
1117
+ const capability = this.#sealCapability();
1118
+ const stepCriteria = input.stepCriteria ?? input.criteria;
1119
+ const assessmentId = `goal-assessment-${acceptanceDigest([record.scope, record.id, record.definition, 'initial'])}`;
1120
+ const task = { kind: 'goal-outcome', ref: assessmentId, goal: {
1121
+ id: record.id, definitionVersion: record.definition.version, definitionDigest: record.definition.digest,
1122
+ assessmentId, sessionId: record.native.sessionId, nativeGoalId: record.native.goalId
1123
+ } };
1124
+ const request = Object.freeze({
1125
+ scope: Object.freeze({ workspace: record.scope.workspace, preset: record.scope.preset }),
1126
+ owner: Object.freeze({ principalRecordId: record.scope.principalRecordId, principalVersion: record.scope.principalVersion }),
1127
+ task, objective: record.definition.objective, criteria: input.criteria, bounds: input.bounds,
1128
+ validityMs: input.validityMs, preparer: input.preparer,
1129
+ });
1130
+ const handle = capability.prepared.prepareOwnerContract(request);
1131
+ const contract = capability.verifier?.inspectAcceptedTask?.(handle.contractId)?.contract;
1132
+ if (contract === undefined || contract.id !== handle.contractId || contract.digest !== handle.contractDigest
1133
+ || contract.task.kind !== 'goal-outcome' || acceptanceDigest(contract.task) !== acceptanceDigest(task)
1134
+ || contract.objective !== record.definition.objective
1135
+ || acceptanceDigest(contract.criteria) !== acceptanceDigest(input.criteria)
1136
+ || acceptanceDigest(contract.bounds) !== acceptanceDigest(input.bounds)
1137
+ || contract.profile.id !== 'owner-prepared' || contract.profile.version !== 1
1138
+ || contract.expiresAt <= Date.now())
1139
+ throw new Error('assistant-goals: sealed owner acceptance readback failed');
1140
+ const seal = validateOwnerAcceptanceSeal({
1141
+ goalId: record.id, definition: record.definition, objective: record.definition.objective,
1142
+ criteria: input.criteria, stepCriteria, bounds: input.bounds, validityMs: input.validityMs,
1143
+ preparer: input.preparer, outcome: handle, preparedProfileDigest: contract.profile.digest,
1144
+ outcomeExpiresAt: contract.expiresAt, at: Date.now(),
1145
+ });
1146
+ // Bind the frozen definition before persisting the seal: either both land or
1147
+ // the caller rolls the native goal back, never a half-sealed goal.
1148
+ this.#outcome.bindPrepared(record, seal);
1149
+ return capability.store.record(seal);
1150
+ }
744
1151
  control = (agent, value) => {
745
1152
  const input = this.#controlInput(value);
746
1153
  const scope = this.#scope(agent, input.operation);
@@ -749,6 +1156,12 @@ export class AssistantGoalsService extends Service {
749
1156
  const record = this.#store.get(scope, input.goalId);
750
1157
  if (record === undefined)
751
1158
  throw new Error('assistant-goals: goal not found');
1159
+ // A frozen owner intent binds the objective to the authenticated owner's
1160
+ // words. Editing it would silently break that proof and orphan the ledger
1161
+ // row, so the caller must create a new goal instead.
1162
+ if (input.operation === 'edit' && this.#ownerIntent?.store.inspect(input.goalId) !== undefined) {
1163
+ throw new Error('assistant-goals: the objective is bound to the authenticated owner request; create a new goal to change it');
1164
+ }
752
1165
  const native = this.ctx.get('goals');
753
1166
  if (native === undefined)
754
1167
  throw new Error('assistant-goals: native goal service unavailable');
@@ -2024,7 +2437,13 @@ export class AssistantGoalsService extends Service {
2024
2437
  execution();
2025
2438
  throw error;
2026
2439
  }
2027
- return () => { outcome?.(); execution(); };
2440
+ this.#acceptanceRegistration = registration;
2441
+ return () => {
2442
+ if (this.#acceptanceRegistration === registration)
2443
+ this.#acceptanceRegistration = undefined;
2444
+ outcome?.();
2445
+ execution();
2446
+ };
2028
2447
  };
2029
2448
  currentArtifactAdmission = (agent) => this.#execution.currentArtifactAdmission(agent);
2030
2449
  inspectAcceptedArtifactSource = (contract) => contract.task.kind === 'goal-outcome'