diff --git a/.github/workflows/at_libraries.yaml b/.github/workflows/at_libraries.yaml
index e2dd3923..c594d93b 100644
--- a/.github/workflows/at_libraries.yaml
+++ b/.github/workflows/at_libraries.yaml
@@ -66,6 +66,7 @@ jobs:
- at_commons
- at_utils
- at_cli_commons
+ - at_policy
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/packages/at_policy/.gitignore b/packages/at_policy/.gitignore
new file mode 100644
index 00000000..3cceda55
--- /dev/null
+++ b/packages/at_policy/.gitignore
@@ -0,0 +1,7 @@
+# https://dart.dev/guides/libraries/private-files
+# Created by `dart pub`
+.dart_tool/
+
+# Avoid committing pubspec.lock for library packages; see
+# https://dart.dev/guides/libraries/private-files#pubspeclock.
+pubspec.lock
diff --git a/packages/at_policy/CHANGELOG.md b/packages/at_policy/CHANGELOG.md
new file mode 100644
index 00000000..effe43c8
--- /dev/null
+++ b/packages/at_policy/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+- Initial version.
diff --git a/packages/at_policy/LICENSE b/packages/at_policy/LICENSE
new file mode 100644
index 00000000..caf63d72
--- /dev/null
+++ b/packages/at_policy/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2020, The @ Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/at_policy/README.md b/packages/at_policy/README.md
new file mode 100644
index 00000000..61358f02
--- /dev/null
+++ b/packages/at_policy/README.md
@@ -0,0 +1,24 @@
+
+
+[![pub package](https://img.shields.io/pub/v/at_policy)](https://pub.dev/packages/at_policy)
+[![pub points](https://img.shields.io/pub/points/at_policy?logo=dart)](https://pub.dev/packages/at_onboarding_cli/score)
+[![gitHub license](https://img.shields.io/badge/license-BSD3-blue.svg)](./LICENSE)
+
+# at_policy
+
+## Introduction
+The at_policy library provides generic scaffolding for building policy
+management services which policy enforcement endpoints communicate with via
+atProtocol and therefore get all the benefits of using atProtocol - outbound
+communication only, end-to-end encryption, using atSigns rather than IP
+addresses
+
+## Usage
+See full worked examples in the [example](example/README.md) directory.
+
+## Open source usage and contributions
+
+This is freely licensed open source code, so feel free to use it as is, suggest
+changes or enhancements or create your own version.
+See [CONTRIBUTING.md](../../CONTRIBUTING.md) for detailed guidance on how to
+set up tools, tests and make a pull request.
\ No newline at end of file
diff --git a/packages/at_policy/analysis_options.yaml b/packages/at_policy/analysis_options.yaml
new file mode 100644
index 00000000..30e7e470
--- /dev/null
+++ b/packages/at_policy/analysis_options.yaml
@@ -0,0 +1,19 @@
+# Defines a default set of lint rules enforced for
+# projects at Google. For details and rationale,
+# see https://pub.dev/packages/lints.
+include: package:lints/recommended.yaml
+
+# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
+# Uncomment to specify additional rules.
+linter:
+ rules:
+ annotate_overrides: true
+ prefer_final_fields: true
+ implicit_call_tearoffs: true
+ camel_case_types : true
+ unnecessary_string_interpolations : true
+ unnecessary_brace_in_string_interps: true
+ await_only_futures : true
+ unawaited_futures: true
+ depend_on_referenced_packages : false
+ avoid_function_literals_in_foreach_calls: true
diff --git a/packages/at_policy/example/.gitignore b/packages/at_policy/example/.gitignore
new file mode 100644
index 00000000..3a857904
--- /dev/null
+++ b/packages/at_policy/example/.gitignore
@@ -0,0 +1,3 @@
+# https://dart.dev/guides/libraries/private-files
+# Created by `dart pub`
+.dart_tool/
diff --git a/packages/at_policy/example/CHANGELOG.md b/packages/at_policy/example/CHANGELOG.md
new file mode 100644
index 00000000..effe43c8
--- /dev/null
+++ b/packages/at_policy/example/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+- Initial version.
diff --git a/packages/at_policy/example/README.md b/packages/at_policy/example/README.md
new file mode 100644
index 00000000..a03c2197
--- /dev/null
+++ b/packages/at_policy/example/README.md
@@ -0,0 +1,23 @@
+# at_policy examples
+
+## Overview
+- Imagine a client which is making a request to some service; it can make
+ three types of request
+ - `getPublicInfo`
+ - `getProtectedInfo`
+ - `getConfidentialInfo`
+- The service needs to make a policy decision for each request - should it
+ respond with the info, or respond with a "not permitted" error? It
+ asks `policy` for information about how it should respond
+
+## Programmes
+- `client.dart`
+ - sends requests to `service`
+- `service.dart`
+ - listens for requests from `client` and determines client intent
+ - sends message to `policy` requesting info about what policy decision it
+ should make regarding the client's intent
+- `policy.dart`
+ - listens for requests from `service` with policy intents
+ - responds with the info that `service` requires in order to be able to
+ make a policy decision
diff --git a/packages/at_policy/example/analysis_options.yaml b/packages/at_policy/example/analysis_options.yaml
new file mode 100644
index 00000000..30e7e470
--- /dev/null
+++ b/packages/at_policy/example/analysis_options.yaml
@@ -0,0 +1,19 @@
+# Defines a default set of lint rules enforced for
+# projects at Google. For details and rationale,
+# see https://pub.dev/packages/lints.
+include: package:lints/recommended.yaml
+
+# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
+# Uncomment to specify additional rules.
+linter:
+ rules:
+ annotate_overrides: true
+ prefer_final_fields: true
+ implicit_call_tearoffs: true
+ camel_case_types : true
+ unnecessary_string_interpolations : true
+ unnecessary_brace_in_string_interps: true
+ await_only_futures : true
+ unawaited_futures: true
+ depend_on_referenced_packages : false
+ avoid_function_literals_in_foreach_calls: true
diff --git a/packages/at_policy/example/bin/client.dart b/packages/at_policy/example/bin/client.dart
new file mode 100644
index 00000000..e82d67e5
--- /dev/null
+++ b/packages/at_policy/example/bin/client.dart
@@ -0,0 +1,55 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:at_cli_commons/at_cli_commons.dart';
+import 'package:at_client/at_client.dart';
+import 'package:chalkdart/chalk.dart';
+import 'common.dart';
+
+void main(List args) async {
+ try {
+ var argsParser = CLIBase.argsParser
+ ..addOption('service-atsign',
+ mandatory: true,
+ help: "the atSign of the service this client is using");
+
+ var serviceAtsign = argsParser.parse(args)['service-atsign'];
+
+ var atClient = (await CLIBase.fromCommandLineArgs(args)).atClient;
+
+ var rpc = AtRpcClient(
+ atClient: atClient,
+ baseNameSpace: atClient.getPreferences()!.namespace!,
+ domainNameSpace: policyRequestNamespace,
+ serverAtsign: serviceAtsign);
+
+ stdout.writeln('Can make three types of request.');
+ for (final rt in RequestType.values) {
+ stdout.writeln(' ${rt.index + 1}. ${rt.name}');
+ }
+ stdout.writeln();
+ while (true) {
+ stdout.write(chalk.brightBlue('Enter request type number: '));
+ int reqType = int.parse(stdin.readLineSync()!.trim());
+ if (reqType < 1 || reqType > RequestType.values.length) {
+ stdout.writeln(chalk.red('Invalid request type'));
+ continue;
+ }
+
+ try {
+ var response = await rpc
+ .call({'reqType': RequestType.values[reqType - 1].name}).timeout(
+ Duration(seconds: 15));
+
+ stdout.writeln(chalk.green(response));
+ } on TimeoutException {
+ stderr.writeln(chalk.brightRed('timed out waiting for response'));
+ } catch (e) {
+ stderr.writeln(chalk.brightRed(e));
+ }
+ }
+ } catch (e) {
+ print(e);
+ print(CLIBase.argsParser.usage);
+ }
+}
diff --git a/packages/at_policy/example/bin/common.dart b/packages/at_policy/example/bin/common.dart
new file mode 100644
index 00000000..4d663507
--- /dev/null
+++ b/packages/at_policy/example/bin/common.dart
@@ -0,0 +1,7 @@
+const String policyRequestNamespace = 'at_policy_example';
+
+enum RequestType {
+ getPublicInfo,
+ getProtectedInfo,
+ getConfidentialInfo,
+}
diff --git a/packages/at_policy/example/bin/policy.dart b/packages/at_policy/example/bin/policy.dart
new file mode 100644
index 00000000..9c17cb0e
--- /dev/null
+++ b/packages/at_policy/example/bin/policy.dart
@@ -0,0 +1,56 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:at_cli_commons/at_cli_commons.dart';
+import 'package:at_policy/at_policy.dart';
+import 'package:chalkdart/chalk.dart';
+
+void main(List args) async {
+ try {
+ var atClient = (await CLIBase.fromCommandLineArgs(args)).atClient;
+
+ PolicyService ps = PolicyService(
+ baseNamespace: atClient.getPreferences()!.namespace!,
+ loggingAtsign: atClient.getCurrentAtSign()!,
+ allowList: {},
+ allowAll: true,
+ atClient: atClient,
+ handler: DemoPolicyRequestHandler(),
+ );
+
+ await ps.run();
+ } catch (e) {
+ print(e);
+ print(CLIBase.argsParser.usage);
+ }
+}
+
+class DemoPolicyRequestHandler implements PolicyRequestHandler {
+ @override
+ Future getPolicyDetails(PolicyRequest req) async {
+ stdout.writeln(chalk.blue('Received request $req'));
+
+ stdout.write('(A)pprove or (D)eny? : ');
+ String decision = '';
+ while (decision.isEmpty) {
+ decision = stdin.readLineSync()!;
+ }
+ final bool approved = decision.toLowerCase().startsWith('a');
+
+ if (approved) {
+ List details = [];
+ for (var i in req.intents) {
+ details.add(PolicyDetail(
+ intent: i.intent,
+ info: {'authorized': true},
+ ));
+ }
+ return PolicyResponse(
+ message: 'manually approved',
+ policyDetails: details,
+ );
+ } else {
+ return PolicyResponse(message: 'nope', policyDetails: []);
+ }
+ }
+}
diff --git a/packages/at_policy/example/bin/service.dart b/packages/at_policy/example/bin/service.dart
new file mode 100644
index 00000000..50fc3453
--- /dev/null
+++ b/packages/at_policy/example/bin/service.dart
@@ -0,0 +1,97 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:at_cli_commons/at_cli_commons.dart';
+import 'package:at_client/at_client.dart';
+import 'package:at_policy/at_policy.dart';
+import 'package:chalkdart/chalk.dart';
+import 'common.dart';
+
+void main(List args) async {
+ try {
+ var argsParser = CLIBase.argsParser
+ ..addOption('policy-atsign',
+ mandatory: true, help: "the atSign of the policy service");
+
+ var policyAtsign = argsParser.parse(args)['policy-atsign'];
+
+ var atClient = (await CLIBase.fromCommandLineArgs(args)).atClient;
+
+ var policyRpcClient = AtRpcClient(
+ atClient: atClient,
+ baseNameSpace: atClient.getPreferences()!.namespace!,
+ domainNameSpace: policyRequestNamespace,
+ serverAtsign: policyAtsign,
+ );
+
+ var thisRpcServer = AtRpc(
+ atClient: atClient,
+ baseNameSpace: atClient.getPreferences()!.namespace!,
+ domainNameSpace: policyRequestNamespace,
+ callbacks: DemoRpcServer(
+ myAtsign: atClient.getCurrentAtSign()!,
+ policyAtsign: policyAtsign,
+ policyRpcClient: policyRpcClient,
+ ),
+ allowList: {},
+ allowAll: true,
+ );
+
+ thisRpcServer.start();
+ } catch (e) {
+ print(e);
+ print(CLIBase.argsParser.usage);
+ }
+}
+
+class DemoRpcServer implements AtRpcCallbacks {
+ String myAtsign;
+ String policyAtsign;
+ AtRpcClient policyRpcClient;
+
+ DemoRpcServer({
+ required this.myAtsign,
+ required this.policyAtsign,
+ required this.policyRpcClient,
+ });
+
+ @override
+ Future handleRequest(AtRpcReq request, String fromAtSign) async {
+ stdout.writeln(chalk.blue('Received request ${request.toJson()}'));
+ RequestType rt;
+
+ try {
+ rt = RequestType.values.byName(request.payload['reqType']);
+ } catch (e) {
+ return AtRpcResp(
+ reqId: request.reqId,
+ respType: AtRpcRespType.error,
+ payload: {},
+ message: 'Invalid request ${request.payload}');
+ }
+
+ PolicyRequest polReq = PolicyRequest(
+ serviceAtsign: myAtsign,
+ serviceName: 'example_service_001',
+ serviceGroupName: 'example_service',
+ clientAtsign: fromAtSign,
+ intents: [PolicyIntent(intent: rt.name, params: {})]);
+
+ stderr.writeln('Sending policy check request : $polReq');
+ Map policyResponse =
+ await policyRpcClient.call(polReq.toJson());
+ stderr.writeln('Received policy info : $policyResponse');
+ return AtRpcResp(
+ reqId: request.reqId,
+ respType: AtRpcRespType.success,
+ payload: {'some': 'payload'},
+ message: 'Not yet implemented', // TODO
+ );
+ }
+
+ @override
+ Future handleResponse(AtRpcResp response) async {
+ // Not expecting to receive responses
+ throw UnimplementedError();
+ }
+}
diff --git a/packages/at_policy/example/pubspec.yaml b/packages/at_policy/example/pubspec.yaml
new file mode 100644
index 00000000..62e0dc0c
--- /dev/null
+++ b/packages/at_policy/example/pubspec.yaml
@@ -0,0 +1,18 @@
+name: at_policy_example
+description: Simple at_policy usage examples
+version: 1.0.0
+publish_to: none
+
+environment:
+ sdk: ^3.0.6
+
+# Add regular dependencies here.
+dependencies:
+ at_policy:
+ path: ..
+ at_cli_commons: ^1.2.1
+ at_client: ^3.3.0
+
+dev_dependencies:
+ lints: ^3.0.0
+ test: ^1.24.9
diff --git a/packages/at_policy/lib/at_policy.dart b/packages/at_policy/lib/at_policy.dart
new file mode 100644
index 00000000..fbf7cf0b
--- /dev/null
+++ b/packages/at_policy/lib/at_policy.dart
@@ -0,0 +1,5 @@
+/// Policy management via atProtocol
+library;
+
+export 'src/policy/interfaces.dart';
+export 'src/policy/models.dart';
diff --git a/packages/at_policy/lib/src/policy/impl.dart b/packages/at_policy/lib/src/policy/impl.dart
new file mode 100644
index 00000000..2dfc79c7
--- /dev/null
+++ b/packages/at_policy/lib/src/policy/impl.dart
@@ -0,0 +1,168 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:at_client/at_client.dart' hide StringBuffer;
+import 'package:at_client/at_client_mixins.dart';
+import 'package:at_policy/at_policy.dart';
+import 'package:at_utils/at_logger.dart';
+
+///
+class PolicyServiceImpl with AtClientBindings implements PolicyService {
+ @override
+ final AtSignLogger logger = AtSignLogger(' PolicyServiceImpl ');
+
+ @override
+ late AtClient atClient;
+
+ @override
+ final PolicyRequestHandler handler;
+
+ @override
+ final String baseNamespace;
+
+ @override
+ final String policyRequestNamespace;
+
+ @override
+ String get policyAtsign => atClient.getCurrentAtSign()!;
+
+ @override
+ final String loggingAtsign;
+
+ @override
+ final Set allowList;
+
+ @override
+ final bool allowAll;
+
+ @override
+ RpcTransformer? requestTransformer;
+
+ @override
+ RpcTransformer? responseTransformer;
+
+ late final AtRpc rpc;
+
+ static const JsonEncoder jsonPrettyPrinter = JsonEncoder.withIndent(' ');
+
+ PolicyServiceImpl({
+ required this.atClient,
+ required this.handler,
+ required this.baseNamespace,
+ required this.policyRequestNamespace,
+ required this.loggingAtsign,
+ required this.allowList,
+ required this.allowAll,
+ this.requestTransformer,
+ this.responseTransformer,
+ }) {
+ rpc = AtRpc(
+ atClient: atClient,
+ baseNameSpace: baseNamespace,
+ domainNameSpace: policyRequestNamespace,
+ callbacks: this,
+ allowList: allowList,
+ allowAll: allowAll,
+ );
+ }
+
+ @override
+ Future run() async {
+ rpc.start();
+
+ logger.info('Listening for requests at '
+ '${rpc.domainNameSpace}.${rpc.rpcsNameSpace}.${rpc.baseNameSpace}');
+ }
+
+ @override
+ Future handleRequest(
+ AtRpcReq rpcRequest, String fromAtSign) async {
+ logger.info('Received request from $fromAtSign: '
+ '${jsonPrettyPrinter.convert(rpcRequest.toJson())}');
+
+ Map requestPayload = rpcRequest.payload;
+ if (requestTransformer != null) {
+ requestPayload = await requestTransformer!(requestPayload);
+ }
+ PolicyRequest policyRequest;
+
+ try {
+ policyRequest = PolicyRequest.fromJson(requestPayload);
+ } catch (e) {
+ final msg = 'Failed PolicyRequest.fromJson: ${e.toString()}';
+ logger.severe(msg);
+ return AtRpcResp.nack(request: rpcRequest, message: msg);
+ }
+
+ try {
+ // We will send a 'log' notification to the loggingAtsign
+ var logKey = AtKey()
+ ..key = '${DateTime.now().millisecondsSinceEpoch}.logs.policy'
+ ..sharedBy = policyAtsign
+ ..sharedWith = loggingAtsign
+ ..namespace = baseNamespace
+ ..metadata = (Metadata()
+ ..isPublic = false
+ ..isEncrypted = true
+ ..namespaceAware = true);
+
+ final event = PolicyLogEvent(
+ timestamp: DateTime.now().millisecondsSinceEpoch,
+ serviceAtsign: fromAtSign,
+ policyAtsign: atClient.getCurrentAtSign(),
+ serviceName: policyRequest.serviceName,
+ serviceGroupName: policyRequest.serviceGroupName,
+ clientAtsign: policyRequest.clientAtsign,
+ eventType: PolicyLogEventType.request,
+ eventDetails: {'intents': policyRequest.intents},
+ message: '',
+ );
+ await notify(
+ logKey,
+ jsonEncode(event),
+ checkForFinalDeliveryStatus: false,
+ waitForFinalDeliveryStatus: false,
+ ttln: Duration(hours: 1),
+ );
+ } catch (e, st) {
+ logger.severe('Failed to send PolicyLogEvent with exception $e'
+ '\nStackTrace:'
+ '\n$st');
+ }
+
+ PolicyResponse policyResponse;
+ AtRpcResp rpcResponse;
+ try {
+ policyResponse = await handler.getPolicyDetails(policyRequest);
+ Map responsePayload = policyResponse.toJson();
+ if (responseTransformer != null) {
+ responsePayload = await responseTransformer!(responsePayload);
+ }
+ rpcResponse = AtRpcResp(
+ reqId: rpcRequest.reqId,
+ respType: AtRpcRespType.success,
+ payload: responsePayload);
+ } catch (e) {
+ logger.severe('Exception: $e');
+ policyResponse = PolicyResponse(
+ message: 'Exception: $e',
+ policyDetails: [],
+ );
+ Map responsePayload = policyResponse.toJson();
+ if (responseTransformer != null) {
+ responsePayload = await responseTransformer!(responsePayload);
+ }
+ rpcResponse = AtRpcResp(
+ reqId: rpcRequest.reqId,
+ respType: AtRpcRespType.error,
+ payload: responsePayload);
+ }
+
+ return rpcResponse;
+ }
+
+ @override
+ Future handleResponse(AtRpcResp response) {
+ throw UnimplementedError();
+ }
+}
diff --git a/packages/at_policy/lib/src/policy/interfaces.dart b/packages/at_policy/lib/src/policy/interfaces.dart
new file mode 100644
index 00000000..d9df40bc
--- /dev/null
+++ b/packages/at_policy/lib/src/policy/interfaces.dart
@@ -0,0 +1,66 @@
+import 'dart:async';
+
+import 'package:at_client/at_client.dart' hide StringBuffer;
+import 'package:at_utils/at_logger.dart';
+import 'package:at_policy/src/policy/impl.dart';
+import 'package:at_policy/src/policy/models.dart';
+
+abstract class PolicyRequestHandler {
+ Future getPolicyDetails(PolicyRequest req);
+}
+
+typedef RpcTransformer = Future